@bedrock-rbx/ocale 0.1.0-beta.1 → 0.1.0-beta.2

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.
Files changed (58) hide show
  1. package/dist/badges.d.mts +86 -3
  2. package/dist/badges.d.mts.map +1 -1
  3. package/dist/badges.mjs +113 -5
  4. package/dist/badges.mjs.map +1 -1
  5. package/dist/data.generated-BtkDGH8C.d.mts +485 -0
  6. package/dist/data.generated-BtkDGH8C.d.mts.map +1 -0
  7. package/dist/developer-products.d.mts +14 -5
  8. package/dist/developer-products.d.mts.map +1 -1
  9. package/dist/developer-products.mjs +5 -4
  10. package/dist/developer-products.mjs.map +1 -1
  11. package/dist/game-passes.d.mts +83 -2
  12. package/dist/game-passes.d.mts.map +1 -1
  13. package/dist/game-passes.mjs +111 -4
  14. package/dist/game-passes.mjs.map +1 -1
  15. package/dist/index.d.mts +3 -81
  16. package/dist/index.d.mts.map +1 -1
  17. package/dist/index.mjs +2 -2
  18. package/dist/is-date-time-string-Cuf1TaSC.mjs +19 -0
  19. package/dist/is-date-time-string-Cuf1TaSC.mjs.map +1 -0
  20. package/dist/locales.d.mts +2 -0
  21. package/dist/locales.mjs +512 -0
  22. package/dist/locales.mjs.map +1 -0
  23. package/dist/luau-execution.d.mts +62 -0
  24. package/dist/luau-execution.d.mts.map +1 -0
  25. package/dist/luau-execution.mjs +52 -0
  26. package/dist/luau-execution.mjs.map +1 -0
  27. package/dist/places.d.mts +47 -8
  28. package/dist/places.d.mts.map +1 -1
  29. package/dist/places.mjs +39 -13
  30. package/dist/places.mjs.map +1 -1
  31. package/dist/{price-information-CmpscMc4.mjs → price-information-s7DY0GV2.mjs} +2 -2
  32. package/dist/{price-information-CmpscMc4.mjs.map → price-information-s7DY0GV2.mjs.map} +1 -1
  33. package/dist/{rate-limit-BBU_4xnZ.mjs → rate-limit-CKfuhxT1.mjs} +11 -3
  34. package/dist/rate-limit-CKfuhxT1.mjs.map +1 -0
  35. package/dist/rate-limit-DzHBFwps.d.mts +92 -0
  36. package/dist/rate-limit-DzHBFwps.d.mts.map +1 -0
  37. package/dist/{resource-client-CaS_j3yg.mjs → resource-client-Wi4Mwqy5.mjs} +69 -14
  38. package/dist/resource-client-Wi4Mwqy5.mjs.map +1 -0
  39. package/dist/specs-Co6qYp_E.mjs +309 -0
  40. package/dist/specs-Co6qYp_E.mjs.map +1 -0
  41. package/dist/storage.d.mts +374 -0
  42. package/dist/storage.d.mts.map +1 -0
  43. package/dist/storage.mjs +371 -0
  44. package/dist/storage.mjs.map +1 -0
  45. package/dist/types-BZ0959rh.d.mts +149 -0
  46. package/dist/types-BZ0959rh.d.mts.map +1 -0
  47. package/dist/{types-YCTsM8Qd.d.mts → types-Cp8w8uwA.d.mts} +1 -1
  48. package/dist/{types-YCTsM8Qd.d.mts.map → types-Cp8w8uwA.d.mts.map} +1 -1
  49. package/dist/universes.d.mts +37 -12
  50. package/dist/universes.d.mts.map +1 -1
  51. package/dist/universes.mjs +5 -4
  52. package/dist/universes.mjs.map +1 -1
  53. package/dist/{validation-CTZzJhmd.mjs → validation-b7KAoEio.mjs} +2 -2
  54. package/dist/validation-b7KAoEio.mjs.map +1 -0
  55. package/package.json +7 -3
  56. package/dist/rate-limit-BBU_4xnZ.mjs.map +0 -1
  57. package/dist/resource-client-CaS_j3yg.mjs.map +0 -1
  58. package/dist/validation-CTZzJhmd.mjs.map +0 -1
@@ -0,0 +1,92 @@
1
+ import { d as OpenCloudError } from "./types-Cp8w8uwA.mjs";
2
+
3
+ //#region src/errors/api-error.d.ts
4
+ /**
5
+ * Options for constructing an {@link ApiError}.
6
+ */
7
+ interface ApiErrorOptions extends ErrorOptions {
8
+ /** Optional machine-readable error code from the API. */
9
+ code?: string | undefined;
10
+ /** Parsed response body, when present. */
11
+ details?: JSONValue | undefined;
12
+ /** HTTP status code from the API response. */
13
+ statusCode: number;
14
+ }
15
+ /**
16
+ * Thrown when the Roblox Open Cloud API returns a non-2xx response
17
+ * that is not a rate limit (429).
18
+ *
19
+ * @example
20
+ *
21
+ * ```ts
22
+ * import { ApiError } from "@bedrock-rbx/ocale";
23
+ *
24
+ * const error = new ApiError("HTTP 404: Pass not found (code NotFound)", {
25
+ * code: "NotFound",
26
+ * details: { errorCode: "NotFound", message: "Pass not found" },
27
+ * statusCode: 404,
28
+ * });
29
+ *
30
+ * expect(error).toBeInstanceOf(ApiError);
31
+ * expect(error.statusCode).toBe(404);
32
+ * expect(error.code).toBe("NotFound");
33
+ * expect(error.details).toEqual({
34
+ * errorCode: "NotFound",
35
+ * message: "Pass not found",
36
+ * });
37
+ * ```
38
+ */
39
+ declare class ApiError extends OpenCloudError {
40
+ readonly code: string | undefined;
41
+ readonly details: JSONValue | undefined;
42
+ override readonly name: string;
43
+ readonly statusCode: number;
44
+ /**
45
+ * Creates a new ApiError.
46
+ *
47
+ * @param message - Human-readable error description.
48
+ * @param options - Error options including status code, optional error
49
+ * code, and the parsed response body when present.
50
+ */
51
+ constructor(message: string, options: ApiErrorOptions);
52
+ }
53
+ //#endregion
54
+ //#region src/errors/rate-limit.d.ts
55
+ /**
56
+ * Options for constructing a {@link RateLimitError}.
57
+ */
58
+ interface RateLimitErrorOptions extends ErrorOptions {
59
+ /** Seconds to wait before retrying the request. */
60
+ retryAfterSeconds: number;
61
+ }
62
+ /**
63
+ * Thrown when the Roblox Open Cloud API returns a 429 Too Many Requests response.
64
+ * Contains the server-suggested retry delay.
65
+ *
66
+ * @example
67
+ *
68
+ * ```ts
69
+ * import { RateLimitError } from "@bedrock-rbx/ocale";
70
+ *
71
+ * const error = new RateLimitError("Too many requests", {
72
+ * retryAfterSeconds: 30,
73
+ * });
74
+ *
75
+ * expect(error).toBeInstanceOf(RateLimitError);
76
+ * expect(error.retryAfterSeconds).toBe(30);
77
+ * ```
78
+ */
79
+ declare class RateLimitError extends OpenCloudError {
80
+ override readonly name = "RateLimitError";
81
+ readonly retryAfterSeconds: number;
82
+ /**
83
+ * Creates a new RateLimitError.
84
+ *
85
+ * @param message - Human-readable error description.
86
+ * @param options - Error options including the retry delay.
87
+ */
88
+ constructor(message: string, options: RateLimitErrorOptions);
89
+ }
90
+ //#endregion
91
+ export { ApiErrorOptions as i, RateLimitErrorOptions as n, ApiError as r, RateLimitError as t };
92
+ //# sourceMappingURL=rate-limit-DzHBFwps.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-DzHBFwps.d.mts","names":[],"sources":["../src/errors/api-error.ts","../src/errors/rate-limit.ts"],"mappings":";;;;;AAKA;UAAiB,eAAA,SAAwB,YAAA;;EAExC,IAAA;;EAEA,OAAA,GAAU,SAAA;;EAEV,UAAA;AAAA;;;AA2BD;;;;;;;;;;;;;;;;;;;;;;cAAa,QAAA,SAAiB,cAAA;EAAA,SACb,IAAA;EAAA,SACA,OAAA,EAAS,SAAA;EAAA,kBACA,IAAA;EAAA,SACT,UAAA;ECnChB;AAoBD;;;;;;EDwBC,WAAA,CAAY,OAAA,UAAiB,OAAA,EAAS,eAAA;AAAA;;;;;AA9CvC;UCAiB,qBAAA,SAA8B,YAAA;;EAE9C,iBAAA;AAAA;;;;;;;AD+BD;;;;;;;;;;;cCXa,cAAA,SAAuB,cAAA;EAAA,kBACV,IAAA;EAAA,SACT,iBAAA;;;;;;;EAQhB,WAAA,CAAY,OAAA,UAAiB,OAAA,EAAS,qBAAA;AAAA"}
@@ -1,4 +1,4 @@
1
- import { i as ApiError, n as PermissionError, r as NetworkError, t as RateLimitError } from "./rate-limit-BBU_4xnZ.mjs";
1
+ import { i as ApiError, n as PermissionError, r as NetworkError, t as RateLimitError } from "./rate-limit-CKfuhxT1.mjs";
2
2
  import { setTimeout } from "node:timers/promises";
3
3
  //#region src/internal/utils/is-record.ts
4
4
  /**
@@ -364,16 +364,37 @@ function headersToRecord(headers) {
364
364
  return Object.fromEntries(headers);
365
365
  }
366
366
  /**
367
- * Permissively extracts a top-level `errorCode` string field from a
368
- * response body.
367
+ * Permissively extracts a machine-readable error code from a response body.
368
+ *
369
+ * Modern Open Cloud responses use `{ errorCode: string, message: string }`;
370
+ * the legacy game-internationalization endpoints use
371
+ * `{ errors: [{ code: number, message: string }, ...] }`. Both shapes are
372
+ * checked; numeric legacy codes are returned as strings so callers see one
373
+ * consistent type.
369
374
  *
370
375
  * @param body - The parsed response body (unknown shape).
371
- * @returns The `errorCode` string if present, otherwise `undefined`.
376
+ * @returns The error code if present, otherwise `undefined`.
372
377
  */
373
378
  function extractErrorCode(body) {
374
379
  if (body === null || typeof body !== "object") return;
375
380
  const errorCode = Reflect.get(body, "errorCode");
376
- return typeof errorCode === "string" ? errorCode : void 0;
381
+ if (typeof errorCode === "string") return errorCode;
382
+ return extractLegacyCode(body);
383
+ }
384
+ /**
385
+ * Permissively extracts a human-readable error message from a response body.
386
+ *
387
+ * Modern Open Cloud responses expose `message` at the top level; the legacy
388
+ * game-internationalization endpoints nest it under `errors[0].message`.
389
+ *
390
+ * @param body - The parsed response body (unknown shape).
391
+ * @returns The message if present, otherwise `undefined`.
392
+ */
393
+ function extractErrorMessage(body) {
394
+ if (body === null || typeof body !== "object") return;
395
+ const message = Reflect.get(body, "message");
396
+ if (typeof message === "string") return message;
397
+ return extractLegacyMessage(body);
377
398
  }
378
399
  /**
379
400
  * Parses the `x-ratelimit-reset` header value into seconds.
@@ -440,18 +461,46 @@ function createFetchHttpClient(fetchFunc = globalThis.fetch) {
440
461
  return classifyResponse(fetchResult.data);
441
462
  } };
442
463
  }
464
+ function readLegacyErrorEntry(body) {
465
+ const errors = Reflect.get(body, "errors");
466
+ if (!Array.isArray(errors)) return;
467
+ const [first] = errors;
468
+ if (typeof first !== "object" || first === null) return;
469
+ return first;
470
+ }
471
+ function extractLegacyCode(body) {
472
+ const first = readLegacyErrorEntry(body);
473
+ if (first === void 0) return;
474
+ const code = Reflect.get(first, "code");
475
+ if (typeof code === "string") return code;
476
+ return typeof code === "number" ? String(code) : void 0;
477
+ }
478
+ function extractLegacyMessage(body) {
479
+ const first = readLegacyErrorEntry(body);
480
+ if (first === void 0) return;
481
+ const message = Reflect.get(first, "message");
482
+ return typeof message === "string" ? message : void 0;
483
+ }
484
+ function formatApiErrorMessage(parts) {
485
+ const { code, message, status } = parts;
486
+ const base = `HTTP ${status}`;
487
+ if (message === void 0 && code === void 0) return base;
488
+ if (message === void 0) return `${base} (code ${code})`;
489
+ if (code === void 0) return `${base}: ${message}`;
490
+ return `${base}: ${message} (code ${code})`;
491
+ }
443
492
  function createApiError(status, body) {
444
- return new ApiError(`HTTP ${status}`, {
445
- code: extractErrorCode(body),
493
+ const code = extractErrorCode(body);
494
+ return new ApiError(formatApiErrorMessage({
495
+ code,
496
+ message: extractErrorMessage(body),
497
+ status
498
+ }), {
499
+ code,
500
+ details: body,
446
501
  statusCode: status
447
502
  });
448
503
  }
449
- /**
450
- * Classifies a fetch `Response` into a typed `Result`.
451
- *
452
- * @param response - The raw fetch Response to classify.
453
- * @returns A Result containing an HttpResponse on success or an OpenCloudError on failure.
454
- */
455
504
  function createRateLimitError(response) {
456
505
  return new RateLimitError("Rate limited", { retryAfterSeconds: parseRetryAfterSeconds(response.headers.get("x-ratelimit-reset") ?? void 0) });
457
506
  }
@@ -469,6 +518,12 @@ async function readResponseBody(response) {
469
518
  };
470
519
  }
471
520
  }
521
+ /**
522
+ * Classifies a fetch `Response` into a typed `Result`.
523
+ *
524
+ * @param response - The raw fetch Response to classify.
525
+ * @returns A Result containing an HttpResponse on success or an OpenCloudError on failure.
526
+ */
472
527
  async function classifyResponse(response) {
473
528
  if (response.status === 429) return {
474
529
  err: createRateLimitError(response),
@@ -649,4 +704,4 @@ function enrichPermissionError(err, spec) {
649
704
  //#endregion
650
705
  export { IDEMPOTENT_METHOD_DEFAULTS as a, CREATE_METHOD_DEFAULTS as i, okRequest as n, isRecord as o, parseEmptyResponse as r, ResourceClient as t };
651
706
 
652
- //# sourceMappingURL=resource-client-CaS_j3yg.mjs.map
707
+ //# sourceMappingURL=resource-client-Wi4Mwqy5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-client-Wi4Mwqy5.mjs","names":["#hooks","#intervalMs","#maxBucketLevel","#sleep","#chain","#waitForToken","#lastCheck","#bucketLevel","#config","#hooks","#httpClient","#queues","#sleep","#getQueue"],"sources":["../src/internal/utils/is-record.ts","../src/internal/http/retry.ts","../src/internal/http/execute.ts","../src/internal/http/rate-limit-queue.ts","../src/internal/utils/try-catch.ts","../src/internal/http/fetch-client.ts","../src/internal/http/resolve-dependencies.ts","../src/internal/resource-client.ts"],"sourcesContent":["/**\n * Narrows `value` to a plain JSON-style record. Excludes arrays, class\n * instances, primitives, and `null`/`undefined`. Used by resource\n * parsers to gate property access on wire bodies whose shape isn't\n * known at compile time.\n *\n * @param value - The unknown value to narrow.\n * @returns `true` when `value` is a plain `[object Object]`.\n */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n\treturn Object.prototype.toString.call(value) === \"[object Object]\";\n}\n","import { ApiError } from \"../../errors/api-error.ts\";\nimport { RateLimitError } from \"../../errors/rate-limit.ts\";\n\n/**\n * Fully-resolved retry config shape that {@link mergeConfig} and\n * {@link shouldRetry} operate on. Fields are required because this represents\n * the post-defaulting, internal view — callers should supply every field (or\n * resolve them via a test factory / client constructor). The partial,\n * user-facing type lives on client construction options; method defaults and\n * per-request overrides use `Partial<RetryResolvable>`.\n */\nexport interface RetryResolvable {\n\t/** Roblox Open Cloud API key. */\n\treadonly apiKey: string;\n\t/** Base URL for the Open Cloud API. */\n\treadonly baseUrl: string;\n\t/** Maximum retry attempts before giving up. */\n\treadonly maxRetries: number;\n\t/** Status codes that are eligible for retry. */\n\treadonly retryableStatuses: ReadonlyArray<number>;\n\t/** Fallback delay function when no server hint is available. */\n\treadonly retryDelay: (attempt: number) => number;\n\t/** Per-request timeout in milliseconds. */\n\treadonly timeout: number;\n}\n\n/**\n * Default retry status codes for idempotent operations (read, list, update,\n * delete). Safe to retry on both rate limits and transient server errors.\n */\nexport const IDEMPOTENT_METHOD_DEFAULTS: Readonly<Pick<RetryResolvable, \"retryableStatuses\">> =\n\tObject.freeze({\n\t\tretryableStatuses: Object.freeze([429, 500, 502, 503, 504] as const),\n\t});\n\n/**\n * Default retry status codes for create operations. Retries rate limits only,\n * to prevent duplicate resources on 5xx (Roblox Open Cloud has no\n * idempotency-key support).\n */\nexport const CREATE_METHOD_DEFAULTS: Readonly<Pick<RetryResolvable, \"retryableStatuses\">> =\n\tObject.freeze({\n\t\tretryableStatuses: Object.freeze([429] as const),\n\t});\n\n/** Kind of HTTP method the merge is being performed for. */\nexport type MethodKind = \"create\" | \"idempotent\";\n\n/**\n * Options for {@link mergeConfig}.\n *\n * @template T - Concrete `RetryResolvable` subtype being merged.\n */\ninterface MergeConfigOptions<T> {\n\t/** Method-level defaults (e.g. {@link CREATE_METHOD_DEFAULTS}). */\n\treadonly methodDefaults: Partial<T>;\n\t/** Whether the method is a create or idempotent operation. */\n\treadonly methodKind: MethodKind;\n\t/** Optional per-request overrides; always win when provided. */\n\treadonly requestOptions?: Partial<T>;\n}\n\n/**\n * Options for {@link computeRetryWaitMs}.\n */\ninterface ComputeRetryWaitMsOptions {\n\t/** Zero-indexed retry attempt number. */\n\treadonly attempt: number;\n\t/** Fallback delay function when no server hint is available. */\n\treadonly retryDelay: (attempt: number) => number;\n}\n\n/**\n * Default exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (capped).\n *\n * @example\n *\n * ```ts\n * import { defaultRetryDelay } from \"./retry\";\n *\n * expect(defaultRetryDelay(0)).toBe(1000);\n * expect(defaultRetryDelay(4)).toBe(16_000);\n * expect(defaultRetryDelay(10)).toBe(30_000);\n * ```\n *\n * @param attempt - Zero-indexed retry attempt number.\n * @returns Wait duration in milliseconds.\n */\nexport function defaultRetryDelay(attempt: number): number {\n\treturn Math.min(1000 * 2 ** attempt, 30_000);\n}\n\n/**\n * Computes how long to wait before the next retry. Prefers the server's\n * suggested delay when the error is a {@link RateLimitError} with a positive\n * `retryAfterSeconds`; otherwise falls through to `retryDelay(attempt)`.\n *\n * @example\n *\n * ```ts\n * import { RateLimitError } from \"../../errors/rate-limit.ts\";\n * import { computeRetryWaitMs, defaultRetryDelay } from \"./retry\";\n *\n * const error = new RateLimitError(\"slow down\", { retryAfterSeconds: 3 });\n *\n * expect(computeRetryWaitMs(error, { attempt: 0, retryDelay: defaultRetryDelay })).toBe(\n * 3000,\n * );\n * ```\n *\n * @example\n *\n * ```ts\n * import { ApiError } from \"../../errors/api-error.ts\";\n * import { computeRetryWaitMs, defaultRetryDelay } from \"./retry\";\n *\n * const error = new ApiError(\"server error\", { statusCode: 503 });\n *\n * expect(computeRetryWaitMs(error, { attempt: 2, retryDelay: defaultRetryDelay })).toBe(\n * 4000,\n * );\n * ```\n *\n * @param error - The error returned by the failing request.\n * @param options - Retry attempt index and fallback delay function.\n * @returns Wait duration in milliseconds before the next attempt.\n */\nexport function computeRetryWaitMs(\n\terror: ApiError | RateLimitError,\n\toptions: ComputeRetryWaitMsOptions,\n): number {\n\tif (error instanceof RateLimitError && error.retryAfterSeconds > 0) {\n\t\treturn error.retryAfterSeconds * 1000;\n\t}\n\n\treturn options.retryDelay(options.attempt);\n}\n\n/**\n * Decides whether a failed request is eligible for retry under the given\n * `retryableStatuses`. Only {@link RateLimitError} (checked against 429) and\n * {@link ApiError} (checked against its `statusCode`) are retryable — network\n * errors and other failures always return `false`.\n *\n * @example\n *\n * ```ts\n * import { RateLimitError } from \"../../errors/rate-limit.ts\";\n * import { shouldRetry } from \"./retry\";\n *\n * const error = new RateLimitError(\"\", { retryAfterSeconds: 1 });\n *\n * expect(shouldRetry(error, { retryableStatuses: [429] })).toBe(true);\n * ```\n *\n * @example\n *\n * ```ts\n * import { ApiError } from \"../../errors/api-error.ts\";\n * import { shouldRetry } from \"./retry\";\n *\n * const error = new ApiError(\"\", { statusCode: 503 });\n *\n * expect(shouldRetry(error, { retryableStatuses: [429, 500, 502, 503, 504] })).toBe(\n * true,\n * );\n * ```\n *\n * @example\n *\n * ```ts\n * import { NetworkError } from \"../../errors/network-error.ts\";\n * import { shouldRetry } from \"./retry\";\n *\n * const error = new NetworkError(\"offline\");\n *\n * expect(shouldRetry(error, { retryableStatuses: [429] })).toBe(false);\n * ```\n *\n * @param error - The error returned by the failing request.\n * @param config - Object carrying the retry-eligible status list.\n * @returns `true` if the error should be retried, `false` otherwise.\n */\nexport function shouldRetry(\n\terror: unknown,\n\tconfig: { readonly retryableStatuses: ReadonlyArray<number> },\n): error is ApiError | RateLimitError {\n\tif (error instanceof RateLimitError) {\n\t\treturn config.retryableStatuses.includes(429);\n\t}\n\n\tif (error instanceof ApiError) {\n\t\treturn config.retryableStatuses.includes(error.statusCode);\n\t}\n\n\treturn false;\n}\n\n/**\n * Resolves the effective config for a single request by shallow-merging the\n * client config, method defaults, and per-request options. Precedence depends\n * on `methodKind`:\n *\n * - `\"create\"`: method defaults override client config, so client-level\n * settings cannot silently relax create-method safety. Only explicit\n * per-request `requestOptions` can.\n * - `\"idempotent\"`: client config overrides method defaults, so consumers\n * can loosen or tighten retry policy globally. `requestOptions` still wins\n * when provided.\n *\n * Array-valued fields like `retryableStatuses` are *replaced*, not extended.\n *\n * @template T - Concrete `RetryResolvable` subtype being merged.\n *\n * @example\n *\n * ```ts\n * import {\n * CREATE_METHOD_DEFAULTS,\n * defaultRetryDelay,\n * mergeConfig,\n * type RetryResolvable,\n * } from \"./retry\";\n *\n * const clientConfig: RetryResolvable = {\n * apiKey: \"k\",\n * baseUrl: \"https://apis.roblox.com\",\n * maxRetries: 3,\n * retryableStatuses: [429, 500],\n * retryDelay: defaultRetryDelay,\n * timeout: 30_000,\n * };\n *\n * const merged = mergeConfig(clientConfig, {\n * methodDefaults: CREATE_METHOD_DEFAULTS,\n * methodKind: \"create\",\n * });\n *\n * expect(merged.retryableStatuses).toStrictEqual([429]);\n * ```\n *\n * @example\n *\n * ```ts\n * import {\n * defaultRetryDelay,\n * IDEMPOTENT_METHOD_DEFAULTS,\n * mergeConfig,\n * type RetryResolvable,\n * } from \"./retry\";\n *\n * const clientConfig: RetryResolvable = {\n * apiKey: \"k\",\n * baseUrl: \"https://apis.roblox.com\",\n * maxRetries: 3,\n * retryableStatuses: [429],\n * retryDelay: defaultRetryDelay,\n * timeout: 30_000,\n * };\n *\n * const merged = mergeConfig(clientConfig, {\n * methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,\n * methodKind: \"idempotent\",\n * requestOptions: { timeout: 10_000 },\n * });\n *\n * expect(merged.retryableStatuses).toStrictEqual([429]);\n * expect(merged.timeout).toBe(10_000);\n * ```\n *\n * @param clientConfig - Config frozen at client construction.\n * @param options - Method defaults, method kind, and optional per-request overrides.\n * @returns A new merged config object. Inputs are not mutated.\n */\nexport function mergeConfig<T extends RetryResolvable>(\n\tclientConfig: T,\n\toptions: MergeConfigOptions<T>,\n): T {\n\tconst { methodDefaults, methodKind, requestOptions } = options;\n\n\tswitch (methodKind) {\n\t\tcase \"create\": {\n\t\t\treturn { ...clientConfig, ...methodDefaults, ...requestOptions };\n\t\t}\n\t\tcase \"idempotent\": {\n\t\t\treturn { ...methodDefaults, ...clientConfig, ...requestOptions };\n\t\t}\n\t\tdefault: {\n\t\t\tconst exhaustive: never = methodKind;\n\t\t\tthrow new Error(`Unexpected methodKind: ${String(exhaustive)}`);\n\t\t}\n\t}\n}\n","import type { OpenCloudError } from \"../../errors/base.ts\";\nimport type { Result } from \"../../types.ts\";\nimport type { SleepFunc } from \"../utils/sleep.ts\";\nimport { computeRetryWaitMs, type RetryResolvable, shouldRetry } from \"./retry.ts\";\nimport type { HttpRequest, HttpResponse, OpenCloudHooks } from \"./types.ts\";\n\n/** A transport callback: takes a request, returns a classified Result. */\ntype SendFunc = (request: HttpRequest) => Promise<Result<HttpResponse, OpenCloudError>>;\n\n/**\n * Inputs to {@link executeWithRetry} bundled as an options object to keep the\n * function signature narrow.\n */\ninterface ExecuteOptions {\n\t/** Fully-resolved retry config (post-merge). */\n\treadonly config: RetryResolvable;\n\t/** Client-level observability hooks. */\n\treadonly hooks: OpenCloudHooks;\n\t/** Transport callback. May be pre-wrapped by a rate-limit queue. */\n\treadonly send: SendFunc;\n\t/** Injectable sleep (tests pass a fake). */\n\treadonly sleep: SleepFunc;\n}\n\n/**\n * Retry-aware orchestration loop. Coordinates a single logical request,\n * looping over `options.send` until it succeeds, the error is non-retryable,\n * or `options.config.maxRetries` is exhausted. Fires observability hooks\n * at each transition. Domain- and queue-agnostic: `send` may be any\n * callback, including one wrapped by a rate-limit queue.\n *\n * @param request - The immutable request to send.\n * @param options - The transport callback, resolved config, hooks, and sleep.\n * @returns The first success, or the final error after retries are exhausted.\n */\nexport async function executeWithRetry(\n\trequest: HttpRequest,\n\toptions: ExecuteOptions,\n): Promise<Result<HttpResponse, OpenCloudError>> {\n\tconst { config, hooks, send, sleep } = options;\n\n\tasync function attempt(): Promise<Result<HttpResponse, OpenCloudError>> {\n\t\thooks.onRequest?.(request);\n\t\treturn send(request);\n\t}\n\n\tlet result = await attempt();\n\n\tfor (let retry = 0; retry < config.maxRetries; retry++) {\n\t\tif (result.success || !shouldRetry(result.err, config)) {\n\t\t\treturn result;\n\t\t}\n\n\t\tconst { err } = result;\n\t\thooks.onRetry?.(retry + 1, err);\n\t\tconst waitMs = computeRetryWaitMs(err, { attempt: retry, retryDelay: config.retryDelay });\n\t\thooks.onRateLimit?.(waitMs);\n\t\tawait sleep(waitMs);\n\n\t\tresult = await attempt();\n\t}\n\n\treturn result;\n}\n","import type { SleepFunc } from \"../utils/sleep.ts\";\nimport type { OpenCloudHooks } from \"./types.ts\";\n\n/**\n * Identifies and bounds a single Roblox Open Cloud operation for rate\n * limiting, e.g. `{ operationKey: \"game-passes.create\", maxPerSecond: 5 }`.\n */\nexport interface OperationLimit {\n\t/** Maximum sustained request rate in requests per second. */\n\treadonly maxPerSecond: number;\n\t/**\n\t * Stable identifier for the operation (e.g. \"game-passes.create\"). Not\n\t * consumed by the queue itself; callers use it to key per-operation\n\t * queues in a registry (see GamePassesClient).\n\t */\n\treadonly operationKey: string;\n}\n\n/**\n * Token-bucket rate limiter for a single `(apiKey, operation)` pair. Every\n * call to `acquire` consumes one token; when the bucket is empty the call\n * waits until a token regenerates before invoking the task. Burst capacity\n * equals `maxPerSecond`, refilling at `maxPerSecond` tokens per second.\n *\n * Implemented as a leaky bucket tracking drain debt in ms. `#lastCheck`\n * advances by `waitMs` after every sleep so the algorithm stays correct\n * whether or not the injected sleep moves `Date.now()` forward.\n */\nexport class RateLimitQueue {\n\treadonly #hooks: OpenCloudHooks;\n\treadonly #intervalMs: number;\n\treadonly #maxBucketLevel: number;\n\treadonly #sleep: SleepFunc;\n\n\t#bucketLevel = 0;\n\t#chain: Promise<void> = Promise.resolve();\n\t#lastCheck: number = Date.now();\n\n\t/**\n\t * Creates a rate-limit queue bound to a single operation.\n\t *\n\t * @param limit - The operation key and its per-second request ceiling.\n\t * @param hooks - Observability callbacks; `onRateLimit` fires when the\n\t * bucket is empty and a sleep is about to start.\n\t * @param sleep - Injectable sleep (tests pass a fake).\n\t */\n\tconstructor(limit: OperationLimit, hooks: OpenCloudHooks, sleep: SleepFunc) {\n\t\tthis.#intervalMs = 1000 / limit.maxPerSecond;\n\t\tthis.#maxBucketLevel = limit.maxPerSecond * this.#intervalMs;\n\t\tthis.#hooks = hooks;\n\t\tthis.#sleep = sleep;\n\t}\n\n\t/**\n\t * Waits for a token — sleeping and firing `hooks.onRateLimit` if the\n\t * bucket is empty — then executes `task`. Concurrent callers are\n\t * serialized at token acquisition; tasks themselves run independently\n\t * once their token is secured.\n\t *\n\t * @param task - The request to run once a token is available.\n\t * @returns The value produced by `task`.\n\t */\n\tpublic async acquire<T>(task: () => Promise<T>): Promise<T> {\n\t\tconst myTurn = this.#chain.then(async () => this.#waitForToken());\n\t\tthis.#chain = myTurn;\n\t\tawait myTurn;\n\t\treturn task();\n\t}\n\n\tasync #waitForToken(): Promise<void> {\n\t\tconst now = Math.max(Date.now(), this.#lastCheck);\n\t\tconst drained = Math.max(0, this.#bucketLevel - (now - this.#lastCheck));\n\t\tthis.#lastCheck = now;\n\n\t\tif (drained + this.#intervalMs <= this.#maxBucketLevel) {\n\t\t\tthis.#bucketLevel = drained + this.#intervalMs;\n\t\t\treturn;\n\t\t}\n\n\t\tconst waitMs = drained + this.#intervalMs - this.#maxBucketLevel;\n\t\tthis.#hooks.onRateLimit?.(waitMs);\n\t\tawait this.#sleep(waitMs);\n\t\tthis.#bucketLevel = this.#maxBucketLevel;\n\t\tthis.#lastCheck = now + waitMs;\n\t}\n}\n","import type { Result } from \"../../types.ts\";\n\n/**\n * Wraps a promise into a {@link Result}, catching rejections.\n *\n * @template T - The resolved value type.\n * @param promise - The promise to wrap.\n * @returns A Result containing the resolved value or the rejection error.\n */\nexport async function tryCatch<T>(promise: Promise<T>): Promise<Result<T>> {\n\ttry {\n\t\tconst data = await promise;\n\t\treturn { data, success: true };\n\t} catch (err) {\n\t\treturn { err: err instanceof Error ? err : new Error(String(err)), success: false };\n\t}\n}\n","import { ApiError } from \"../../errors/api-error.ts\";\nimport type { OpenCloudError } from \"../../errors/base.ts\";\nimport { NetworkError } from \"../../errors/network-error.ts\";\nimport { RateLimitError } from \"../../errors/rate-limit.ts\";\nimport type { Result } from \"../../types.ts\";\nimport { tryCatch } from \"../utils/try-catch.ts\";\nimport type { HttpClient, HttpRequest, HttpResponse, RequestConfig } from \"./types.ts\";\n\ninterface ApiErrorMessageParts {\n\treadonly code: string | undefined;\n\treadonly message: string | undefined;\n\treadonly status: number;\n}\n\n/**\n * Converts a `Headers` object to a plain record with lowercased keys.\n *\n * @param headers - The `Headers` instance to convert.\n * @returns A record mapping lowercased header names to their values.\n */\nexport function headersToRecord(headers: Headers): Record<string, string> {\n\treturn Object.fromEntries(headers);\n}\n\n/**\n * Permissively extracts a machine-readable error code from a response body.\n *\n * Modern Open Cloud responses use `{ errorCode: string, message: string }`;\n * the legacy game-internationalization endpoints use\n * `{ errors: [{ code: number, message: string }, ...] }`. Both shapes are\n * checked; numeric legacy codes are returned as strings so callers see one\n * consistent type.\n *\n * @param body - The parsed response body (unknown shape).\n * @returns The error code if present, otherwise `undefined`.\n */\nexport function extractErrorCode(body: unknown): string | undefined {\n\tif (body === null || typeof body !== \"object\") {\n\t\treturn undefined;\n\t}\n\n\tconst errorCode = Reflect.get(body, \"errorCode\");\n\tif (typeof errorCode === \"string\") {\n\t\treturn errorCode;\n\t}\n\n\treturn extractLegacyCode(body);\n}\n\n/**\n * Permissively extracts a human-readable error message from a response body.\n *\n * Modern Open Cloud responses expose `message` at the top level; the legacy\n * game-internationalization endpoints nest it under `errors[0].message`.\n *\n * @param body - The parsed response body (unknown shape).\n * @returns The message if present, otherwise `undefined`.\n */\nexport function extractErrorMessage(body: unknown): string | undefined {\n\tif (body === null || typeof body !== \"object\") {\n\t\treturn undefined;\n\t}\n\n\tconst message = Reflect.get(body, \"message\");\n\tif (typeof message === \"string\") {\n\t\treturn message;\n\t}\n\n\treturn extractLegacyMessage(body);\n}\n\n/**\n * Parses the `x-ratelimit-reset` header value into seconds.\n *\n * @param headerValue - The raw header value, or `undefined` if missing.\n * @returns The number of seconds to wait, or 0 if missing/invalid.\n */\nexport function parseRetryAfterSeconds(headerValue: string | undefined): number {\n\tconst parsed = Number(headerValue);\n\tif (Number.isNaN(parsed)) {\n\t\treturn 0;\n\t}\n\n\treturn Math.max(0, Math.floor(parsed));\n}\n\n/**\n * Joins the base URL from config with the relative path from the request.\n *\n * @param request - The HTTP request containing the relative URL.\n * @param config - The request config containing the base URL.\n * @returns The fully-qualified URL string.\n */\nexport function buildUrl(request: HttpRequest, config: RequestConfig): string {\n\tconst base = config.baseUrl.endsWith(\"/\") ? config.baseUrl.slice(0, -1) : config.baseUrl;\n\treturn `${base}${request.url}`;\n}\n\n/**\n * Constructs the `RequestInit` options for a `fetch` call.\n *\n * @param request - The HTTP request to build options for.\n * @param config - The request config containing API key and timeout.\n * @returns A `RequestInit` object ready for `fetch`.\n */\nexport function buildFetchOptions(request: HttpRequest, config: RequestConfig): RequestInit {\n\tconst headers = new Headers({\n\t\t\"x-api-key\": config.apiKey,\n\t});\n\n\tconst options: RequestInit = {\n\t\theaders,\n\t\tmethod: request.method,\n\t};\n\n\tif (request.body instanceof FormData) {\n\t\toptions.body = request.body;\n\t} else if (request.body instanceof Uint8Array) {\n\t\theaders.set(\"content-type\", \"application/octet-stream\");\n\t\toptions.body = request.body;\n\t} else if (request.body !== undefined) {\n\t\theaders.set(\"content-type\", \"application/json\");\n\t\toptions.body = JSON.stringify(request.body);\n\t}\n\n\tif (request.headers !== undefined) {\n\t\tfor (const [name, value] of Object.entries(request.headers)) {\n\t\t\tif (name.toLowerCase() === \"x-api-key\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\theaders.set(name, value);\n\t\t}\n\t}\n\n\tif (config.timeout !== undefined) {\n\t\toptions.signal = AbortSignal.timeout(config.timeout);\n\t}\n\n\treturn options;\n}\n\n/**\n * Creates an {@link HttpClient} backed by the Fetch API.\n *\n * @param fetchFunc - The fetch implementation to use. Defaults to `globalThis.fetch`.\n * @returns An HttpClient that classifies responses into typed Results.\n */\nexport function createFetchHttpClient(\n\tfetchFunc: (url: string, init: RequestInit) => Promise<Response> = globalThis.fetch,\n): HttpClient {\n\treturn {\n\t\tasync request(\n\t\t\thttpRequest: HttpRequest,\n\t\t\tconfig: RequestConfig,\n\t\t): Promise<Result<HttpResponse, OpenCloudError>> {\n\t\t\tconst url = buildUrl(httpRequest, config);\n\t\t\tconst options = buildFetchOptions(httpRequest, config);\n\n\t\t\tconst fetchResult = await tryCatch(fetchFunc(url, options));\n\t\t\tif (!fetchResult.success) {\n\t\t\t\treturn {\n\t\t\t\t\terr: new NetworkError(\"Network request failed\", { cause: fetchResult.err }),\n\t\t\t\t\tsuccess: false,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn classifyResponse(fetchResult.data);\n\t\t},\n\t};\n}\n\nfunction readLegacyErrorEntry(body: object): object | undefined {\n\tconst errors = Reflect.get(body, \"errors\");\n\tif (!Array.isArray(errors)) {\n\t\treturn undefined;\n\t}\n\n\tconst [first] = errors;\n\tif (typeof first !== \"object\" || first === null) {\n\t\treturn undefined;\n\t}\n\n\treturn first;\n}\n\nfunction extractLegacyCode(body: object): string | undefined {\n\tconst first = readLegacyErrorEntry(body);\n\tif (first === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst code = Reflect.get(first, \"code\");\n\tif (typeof code === \"string\") {\n\t\treturn code;\n\t}\n\n\treturn typeof code === \"number\" ? String(code) : undefined;\n}\n\nfunction extractLegacyMessage(body: object): string | undefined {\n\tconst first = readLegacyErrorEntry(body);\n\tif (first === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst message = Reflect.get(first, \"message\");\n\treturn typeof message === \"string\" ? message : undefined;\n}\n\nfunction formatApiErrorMessage(parts: ApiErrorMessageParts): string {\n\tconst { code, message, status } = parts;\n\tconst base = `HTTP ${status}`;\n\tif (message === undefined && code === undefined) {\n\t\treturn base;\n\t}\n\n\tif (message === undefined) {\n\t\treturn `${base} (code ${code})`;\n\t}\n\n\tif (code === undefined) {\n\t\treturn `${base}: ${message}`;\n\t}\n\n\treturn `${base}: ${message} (code ${code})`;\n}\n\nfunction createApiError(status: number, body: JSONValue | undefined): ApiError {\n\tconst code = extractErrorCode(body);\n\tconst message = extractErrorMessage(body);\n\treturn new ApiError(formatApiErrorMessage({ code, message, status }), {\n\t\tcode,\n\t\tdetails: body,\n\t\tstatusCode: status,\n\t});\n}\n\nfunction createRateLimitError(response: Response): RateLimitError {\n\treturn new RateLimitError(\"Rate limited\", {\n\t\tretryAfterSeconds: parseRetryAfterSeconds(\n\t\t\tresponse.headers.get(\"x-ratelimit-reset\") ?? undefined,\n\t\t),\n\t});\n}\n\nasync function readResponseBody(\n\tresponse: Response,\n): Promise<Result<JSONValue | undefined, OpenCloudError>> {\n\ttry {\n\t\tconst text = await response.text();\n\t\treturn { data: text === \"\" ? undefined : JSON.parse(text), success: true };\n\t} catch {\n\t\treturn {\n\t\t\terr: new ApiError(\"Failed to parse response body\", { statusCode: response.status }),\n\t\t\tsuccess: false,\n\t\t};\n\t}\n}\n\n/**\n * Classifies a fetch `Response` into a typed `Result`.\n *\n * @param response - The raw fetch Response to classify.\n * @returns A Result containing an HttpResponse on success or an OpenCloudError on failure.\n */\nasync function classifyResponse(response: Response): Promise<Result<HttpResponse, OpenCloudError>> {\n\tif (response.status === 429) {\n\t\treturn { err: createRateLimitError(response), success: false };\n\t}\n\n\tconst bodyResult = await readResponseBody(response);\n\tif (!bodyResult.success) {\n\t\treturn bodyResult;\n\t}\n\n\tif (response.status >= 300) {\n\t\treturn { err: createApiError(response.status, bodyResult.data), success: false };\n\t}\n\n\treturn {\n\t\tdata: {\n\t\t\tbody: bodyResult.data,\n\t\t\theaders: headersToRecord(response.headers),\n\t\t\tstatus: response.status,\n\t\t},\n\t\tsuccess: true,\n\t};\n}\n","import { setTimeout } from \"node:timers/promises\";\n\nimport type { HttpClient, SleepFunc } from \"../../client/types.ts\";\nimport { createFetchHttpClient } from \"./fetch-client.ts\";\n\n/**\n * Options accepted by {@link resolveDependencies}. Mirrors the test-seam\n * subset of the public client options.\n */\ninterface ResolveDependenciesOptions {\n\t/** Test seam: custom {@link HttpClient}. Defaults to a fetch-backed client. */\n\treadonly httpClient?: HttpClient | undefined;\n\t/** Test seam: custom {@link SleepFunc}. Defaults to a `setTimeout`-backed sleep. */\n\treadonly sleep?: SleepFunc | undefined;\n}\n\n/**\n * Fully-populated dependency set consumed by resource client constructors.\n */\ninterface ResolvedDependencies {\n\t/** Concrete {@link HttpClient} implementation. */\n\treadonly httpClient: HttpClient;\n\t/** Concrete {@link SleepFunc} implementation. */\n\treadonly sleep: SleepFunc;\n}\n\n/**\n * Resolves the concrete HTTP client and sleep implementation a resource\n * client should use. Falls back to the fetch-backed HTTP client and the\n * default `setTimeout`-based sleep when the caller omits the test seams.\n *\n * Extracted so resource client constructors can keep their dependency\n * resolution logic in a single, unit-testable place; this makes the\n * default branches easy to cover without stubbing globals like `fetch`.\n *\n * @param options - Optional {@link HttpClient} and {@link SleepFunc} test seams.\n * @returns A {@link ResolvedDependencies} with defaults applied.\n */\nexport function resolveDependencies(options: ResolveDependenciesOptions): ResolvedDependencies {\n\treturn {\n\t\thttpClient: options.httpClient ?? createFetchHttpClient(),\n\t\tsleep: options.sleep ?? setTimeout,\n\t};\n}\n","import type { Except } from \"type-fest\";\n\nimport type {\n\tHttpClient,\n\tHttpRequest,\n\tHttpResponse,\n\tOpenCloudClientOptions,\n\tOpenCloudHooks,\n\tRequestOptions,\n\tSleepFunc,\n} from \"../client/types.ts\";\nimport { ApiError } from \"../errors/api-error.ts\";\nimport type { OpenCloudError } from \"../errors/base.ts\";\nimport { PermissionError } from \"../errors/permission-error.ts\";\nimport type { Result } from \"../types.ts\";\nimport { executeWithRetry } from \"./http/execute.ts\";\nimport { type OperationLimit, RateLimitQueue } from \"./http/rate-limit-queue.ts\";\nimport { resolveDependencies } from \"./http/resolve-dependencies.ts\";\nimport {\n\tdefaultRetryDelay,\n\tIDEMPOTENT_METHOD_DEFAULTS,\n\tmergeConfig,\n\ttype MethodKind,\n\ttype RetryResolvable,\n} from \"./http/retry.ts\";\n\n/**\n * Describes a single resource method's shape for dispatch through\n * `ResourceClient.execute`. Each resource client declares one module-level\n * constant per public method; that constant binds the four resource-specific\n * values (request builder, response parser, retry-policy method kind,\n * operation-level rate limit) and flows through `execute` uniformly.\n *\n * @template P - The resource-specific parameter shape the builder\n * accepts.\n * @template T - The resource-specific parsed success type the parser\n * produces.\n */\nexport interface ResourceMethodSpec<P, T> {\n\t/**\n\t * Builds the pure {@link HttpRequest} for a single call. Returns a\n\t * {@link Result} so a builder can short-circuit with a local error\n\t * (typically a {@link OpenCloudError} subclass such as `ValidationError`)\n\t * before any HTTP, queue, or retry work happens. Builders that cannot\n\t * fail wrap their return as `{ data: request, success: true }`.\n\t */\n\treadonly buildRequest: (parameters: P) => Result<HttpRequest, OpenCloudError>;\n\t/** Method-level retry defaults merged into the resolved config. */\n\treadonly methodDefaults: Partial<RetryResolvable>;\n\t/**\n\t * Method kind, controlling merge precedence: `\"create\"` lets method\n\t * defaults win over client config so create safety cannot be relaxed\n\t * silently; `\"idempotent\"` lets client config win over method defaults\n\t * so consumers can loosen retry globally.\n\t */\n\treadonly methodKind: MethodKind;\n\t/** Operation-level rate limit, keyed into the client's per-key queue map. */\n\treadonly operationLimit: OperationLimit;\n\t/**\n\t * Converts the full {@link HttpResponse} into the resource-specific\n\t * parsed shape. Takes the whole response (body, status, headers) so\n\t * future parsers can read headers without widening the signature.\n\t */\n\treadonly parse: (response: HttpResponse) => Result<T, OpenCloudError>;\n\t/**\n\t * Open Cloud scopes the API key or OAuth token must carry for this\n\t * method, sourced from the vendored OpenAPI schema's `x-roblox-scopes`.\n\t * When set, a 401 or 403 ApiError from the upstream call is upgraded to\n\t * a {@link PermissionError} carrying these scopes alongside\n\t * {@link OperationLimit.operationKey}, so callers can name the missing\n\t * scope instead of just the HTTP status. Optional so test specs and\n\t * not-yet-wired resources can opt out.\n\t */\n\treadonly requiredScopes?: ReadonlyArray<string>;\n}\n\n/**\n * Single-argument bundle consumed by `ResourceClient.execute`: the per-method\n * spec, the resource-specific parameters, and optional per-request config\n * overrides.\n *\n * @template P - The resource-specific parameter shape the builder accepts.\n * @template T - The resource-specific parsed success type the parser produces.\n */\ninterface ExecuteCall<P, T> {\n\t/** Optional per-request config overrides. */\n\treadonly options?: RequestOptions | undefined;\n\t/** Resource-specific request parameters. */\n\treadonly parameters: P;\n\t/** Per-method binding of builder, parser, method kind, and operation limit. */\n\treadonly spec: ResourceMethodSpec<P, T>;\n}\n\n/**\n * Wraps an infallible request build as a {@link Result}-returning\n * `buildRequest` callback compatible with {@link ResourceMethodSpec}.\n * Use from a resource client whose builder cannot fail; resource clients\n * with local validation should construct the {@link Result} directly.\n *\n * @param request - The pre-built {@link HttpRequest}.\n * @returns A success Result wrapping the request.\n */\nexport function okRequest(request: HttpRequest): Result<HttpRequest, OpenCloudError> {\n\treturn { data: request, success: true };\n}\n\n/**\n * A {@link ResourceMethodSpec.parse} implementation for endpoints that return\n * no business payload on success (such as `DELETE` and reorder operations).\n * Surfaces `undefined` data and never inspects the response body.\n *\n * @returns A success Result with `undefined` data.\n */\nexport function parseEmptyResponse(): Result<undefined, OpenCloudError> {\n\treturn { data: undefined, success: true };\n}\n\nconst CLIENT_DEFAULTS = Object.freeze({\n\tbaseUrl: \"https://apis.roblox.com\",\n\tmaxRetries: 3,\n\tretryableStatuses: IDEMPOTENT_METHOD_DEFAULTS.retryableStatuses,\n\tretryDelay: defaultRetryDelay,\n\ttimeout: 30_000,\n} satisfies Except<RetryResolvable, \"apiKey\">);\n\n/**\n * Internal orchestrator shared by every Open Cloud resource client. Holds\n * the frozen client config, observability hooks, injected HTTP client and\n * sleep, and the per-effective-key rate-limit queue registry. Resource\n * classes compose one instance and dispatch every public method through\n * {@link ResourceClient.execute} with a per-method {@link ResourceMethodSpec}.\n * Not exported from any package subpath; reachable only via sibling\n * `src/resources/**` modules in this package.\n */\nexport class ResourceClient {\n\treadonly #config: Readonly<RetryResolvable>;\n\treadonly #hooks: OpenCloudHooks;\n\treadonly #httpClient: HttpClient;\n\treadonly #queues = new Map<string, RateLimitQueue>();\n\treadonly #sleep: SleepFunc;\n\n\t/**\n\t * Creates a new {@link ResourceClient}. Resolves the injected HTTP\n\t * client and sleep (defaulting to fetch + `setTimeout`) and freezes the\n\t * merged client config so subsequent calls cannot mutate it.\n\t *\n\t * @param options - Client-level configuration including the API key\n\t * and optional construction-time test seams.\n\t */\n\tconstructor(options: OpenCloudClientOptions) {\n\t\tconst { apiKey, hooks, httpClient, sleep, ...overrides } = options;\n\t\tconst resolved = resolveDependencies({ httpClient, sleep });\n\t\tthis.#httpClient = resolved.httpClient;\n\t\tthis.#sleep = resolved.sleep;\n\t\tthis.#hooks = hooks ?? {};\n\t\tthis.#config = Object.freeze({\n\t\t\t...CLIENT_DEFAULTS,\n\t\t\tapiKey,\n\t\t\t...overrides,\n\t\t});\n\t}\n\n\t/**\n\t * Dispatches a single resource-method call. Merges the frozen client\n\t * config with the method's `methodDefaults` and the caller's optional\n\t * per-request `options`, routes through the effective-apiKey rate-limit\n\t * queue, runs the retry loop, and finally parses the response with the\n\t * spec's parser.\n\t *\n\t * @param call - The per-method spec, resource-specific parameters, and\n\t * optional per-request overrides.\n\t * @returns The parsed success payload or the {@link OpenCloudError} that\n\t * caused the request to fail. Never throws.\n\t */\n\tpublic async execute<P, T>(call: ExecuteCall<P, T>): Promise<Result<T, OpenCloudError>> {\n\t\tconst { options, parameters, spec } = call;\n\t\tconst merged = mergeConfig(this.#config, {\n\t\t\tmethodDefaults: spec.methodDefaults,\n\t\t\tmethodKind: spec.methodKind,\n\t\t\trequestOptions: options ?? {},\n\t\t});\n\t\tconst requestResult = spec.buildRequest(parameters);\n\t\tif (!requestResult.success) {\n\t\t\treturn requestResult;\n\t\t}\n\n\t\tconst requestConfig = {\n\t\t\tapiKey: merged.apiKey,\n\t\t\tbaseUrl: merged.baseUrl,\n\t\t\ttimeout: merged.timeout,\n\t\t};\n\t\tconst queue = this.#getQueue(merged.apiKey, spec.operationLimit);\n\t\tconst httpResult = await queue.acquire(async () => {\n\t\t\treturn executeWithRetry(requestResult.data, {\n\t\t\t\tconfig: merged,\n\t\t\t\thooks: this.#hooks,\n\t\t\t\tsend: async (toSend) => this.#httpClient.request(toSend, requestConfig),\n\t\t\t\tsleep: this.#sleep,\n\t\t\t});\n\t\t});\n\t\tif (!httpResult.success) {\n\t\t\treturn { err: enrichPermissionError(httpResult.err, spec), success: false };\n\t\t}\n\n\t\treturn spec.parse(httpResult.data);\n\t}\n\n\t#getQueue(apiKey: string, limit: OperationLimit): RateLimitQueue {\n\t\tconst key = `${apiKey}::${limit.operationKey}`;\n\t\tconst existing = this.#queues.get(key);\n\t\tif (existing !== undefined) {\n\t\t\treturn existing;\n\t\t}\n\n\t\tconst queue = new RateLimitQueue(limit, this.#hooks, this.#sleep);\n\t\tthis.#queues.set(key, queue);\n\t\treturn queue;\n\t}\n}\n\nfunction enrichPermissionError<P, T>(\n\terr: OpenCloudError,\n\tspec: ResourceMethodSpec<P, T>,\n): OpenCloudError {\n\tif (spec.requiredScopes === undefined) {\n\t\treturn err;\n\t}\n\n\tif (err instanceof PermissionError) {\n\t\treturn err;\n\t}\n\n\tif (!(err instanceof ApiError)) {\n\t\treturn err;\n\t}\n\n\tif (err.statusCode !== 401 && err.statusCode !== 403) {\n\t\treturn err;\n\t}\n\n\treturn new PermissionError(err.message, {\n\t\tcause: err.cause,\n\t\tcode: err.code,\n\t\toperationKey: spec.operationLimit.operationKey,\n\t\trequiredScopes: spec.requiredScopes,\n\t\tstatusCode: err.statusCode,\n\t});\n}\n"],"mappings":"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"}
@@ -0,0 +1,309 @@
1
+ import { i as ApiError } from "./rate-limit-CKfuhxT1.mjs";
2
+ import { t as ValidationError } from "./validation-b7KAoEio.mjs";
3
+ import { a as IDEMPOTENT_METHOD_DEFAULTS, i as CREATE_METHOD_DEFAULTS, n as okRequest, o as isRecord } from "./resource-client-Wi4Mwqy5.mjs";
4
+ //#region src/domains/cloud-v2/luau-execution-tasks/builders.ts
5
+ const JSON_HEADERS = { "content-type": "application/json" };
6
+ /**
7
+ * Builds a `POST` request for the Open Cloud "create Luau execution
8
+ * session task" endpoint, targeting the place's head version. Serializes
9
+ * `timeoutSeconds` into the wire's duration string format (`"<n>s"`)
10
+ * when supplied.
11
+ *
12
+ * @param parameters - Universe and place identifiers, the script body,
13
+ * and an optional `timeoutSeconds`.
14
+ * @returns A pure {@link HttpRequest} describing the submit call.
15
+ */
16
+ function buildSubmitAtHeadRequest(parameters) {
17
+ const { placeId, universeId } = parameters;
18
+ return {
19
+ body: buildSubmitBody(parameters),
20
+ headers: JSON_HEADERS,
21
+ method: "POST",
22
+ url: `/cloud/v2/universes/${universeId}/places/${placeId}/luau-execution-session-tasks`
23
+ };
24
+ }
25
+ /**
26
+ * Builds a `POST` request for the Open Cloud "create Luau execution
27
+ * session task" endpoint, targeting a specific place version. Differs
28
+ * from {@link buildSubmitAtHeadRequest} only in URL shape: the path
29
+ * includes the `versions/{versionId}` segment so the script runs
30
+ * against that exact place version instead of the live head.
31
+ *
32
+ * @param parameters - Universe, place, and version identifiers, the
33
+ * script body, and an optional `timeoutSeconds`.
34
+ * @returns A pure {@link HttpRequest} describing the submit call.
35
+ */
36
+ function buildSubmitAtVersionRequest(parameters) {
37
+ const { placeId, universeId, versionId } = parameters;
38
+ return {
39
+ body: buildSubmitBody(parameters),
40
+ headers: JSON_HEADERS,
41
+ method: "POST",
42
+ url: `/cloud/v2/universes/${universeId}/places/${placeId}/versions/${versionId}/luau-execution-session-tasks`
43
+ };
44
+ }
45
+ /**
46
+ * Builds a `GET` request for the Open Cloud "read Luau execution session
47
+ * task" endpoint. The endpoint accepts only the maximal x-aep-resource
48
+ * path shape (universe, place, version, session, task), so the supplied
49
+ * ref must include `versionId` and `sessionId`; refs extracted from the
50
+ * narrower path formats are rejected with a {@link ValidationError}.
51
+ *
52
+ * @param parameters - Task ref and optional view selector. When `view`
53
+ * is omitted, no `?view=` query is sent and the server applies its
54
+ * own default (`BASIC`).
55
+ * @returns A success result wrapping the request, or a
56
+ * {@link ValidationError} when the ref is missing `versionId` or
57
+ * `sessionId`.
58
+ */
59
+ function buildGetRequest(parameters) {
60
+ const { ref, view } = parameters;
61
+ const { placeId, sessionId, taskId, universeId, versionId } = ref;
62
+ if (versionId === void 0) return {
63
+ err: new ValidationError("Task ref is missing versionId; cannot GET", { code: "incomplete_ref" }),
64
+ success: false
65
+ };
66
+ if (sessionId === void 0) return {
67
+ err: new ValidationError("Task ref is missing sessionId; cannot GET", { code: "incomplete_ref" }),
68
+ success: false
69
+ };
70
+ const base = `/cloud/v2/universes/${universeId}/places/${placeId}/versions/${versionId}/luau-execution-sessions/${sessionId}/tasks/${taskId}`;
71
+ return {
72
+ data: {
73
+ method: "GET",
74
+ url: view === void 0 ? base : `${base}?view=${view}`
75
+ },
76
+ success: true
77
+ };
78
+ }
79
+ function buildSubmitBody(parameters) {
80
+ const { script, timeoutSeconds } = parameters;
81
+ return timeoutSeconds === void 0 ? { script } : {
82
+ script,
83
+ timeout: `${timeoutSeconds}s`
84
+ };
85
+ }
86
+ //#endregion
87
+ //#region src/domains/cloud-v2/luau-execution-tasks/operations.ts
88
+ const SUBMIT_PER_MINUTE = 40;
89
+ const GET_PER_MINUTE = 200;
90
+ const SECONDS_PER_MINUTE = 60;
91
+ /**
92
+ * Per-second request ceiling for submitting a Luau execution task,
93
+ * sourced from `x-roblox-rate-limits.perApiKeyOwner` on the
94
+ * `Cloud_CreateLuauExecutionSessionTask__Using_Universes` operation
95
+ * (40 requests per minute per API key owner). The two URL shapes
96
+ * (head and version) share this queue because Roblox attributes both
97
+ * to the same per-minute quota.
98
+ */
99
+ const SUBMIT_OPERATION_LIMIT = Object.freeze({
100
+ maxPerSecond: SUBMIT_PER_MINUTE / SECONDS_PER_MINUTE,
101
+ operationKey: "luau-execution-tasks.submit"
102
+ });
103
+ /**
104
+ * Per-second request ceiling for fetching a Luau execution task,
105
+ * sourced from `x-roblox-rate-limits.perApiKeyOwner` on the
106
+ * `Cloud_GetLuauExecutionSessionTask` operation (200 requests per
107
+ * minute per API key owner).
108
+ */
109
+ const GET_OPERATION_LIMIT = Object.freeze({
110
+ maxPerSecond: GET_PER_MINUTE / SECONDS_PER_MINUTE,
111
+ operationKey: "luau-execution-tasks.get"
112
+ });
113
+ /**
114
+ * Scopes required to submit a Luau execution task, sourced from
115
+ * `x-roblox-scopes` on the create operation in the vendored OpenAPI
116
+ * schema. Surfaced via the `requiredScopes` field of the per-method
117
+ * spec so a 401 or 403 ApiError is upgraded to a `PermissionError`
118
+ * naming the missing scope.
119
+ */
120
+ const SUBMIT_REQUIRED_SCOPES = Object.freeze(["universe.place.luau-execution-session:write"]);
121
+ /**
122
+ * Scopes required to fetch a Luau execution task, sourced from
123
+ * `x-roblox-scopes` on the get operation. The `:write` scope also
124
+ * grants read in upstream auth, but we surface only `:read` here as
125
+ * the minimum-privilege requirement for this method.
126
+ */
127
+ const GET_REQUIRED_SCOPES = Object.freeze(["universe.place.luau-execution-session:read"]);
128
+ //#endregion
129
+ //#region src/domains/cloud-v2/luau-execution-tasks/parsers.ts
130
+ const MALFORMED_TASK_MESSAGE = "Malformed luau-execution-session-task response";
131
+ const PATH_PATTERN = /^universes\/(\d+)\/places\/(\d+)(?:\/versions\/(\d+))?(?:\/luau-execution-sessions\/([^/]+)\/tasks\/([^/]+)|\/luau-execution-session-tasks\/([^/]+))$/;
132
+ /**
133
+ * Parses a successful Open Cloud `LuauExecutionSessionTask` response
134
+ * body into the public {@link LuauExecutionTask} discriminated union.
135
+ * Handles every supported task state (in-progress, COMPLETE, FAILED)
136
+ * across all four x-aep-resource path shapes the server returns.
137
+ *
138
+ * @param response - The full {@link HttpResponse} from the Open Cloud
139
+ * API.
140
+ * @returns A success result wrapping the parsed task, or an
141
+ * {@link ApiError} when the body or path do not match a supported
142
+ * shape.
143
+ */
144
+ function parseLuauExecutionTaskResponse(response) {
145
+ const { body, status: statusCode } = response;
146
+ if (!isLuauExecutionTaskWire(body)) return malformed(statusCode);
147
+ const ref = parseTaskRef(body.path);
148
+ if (ref === void 0) return malformed(statusCode);
149
+ const timeoutSeconds = parseTimeoutSeconds(body.timeout);
150
+ if (body.state === "COMPLETE") return parseCompleteTask({
151
+ body,
152
+ ref,
153
+ statusCode,
154
+ timeoutSeconds
155
+ });
156
+ if (body.state === "FAILED") return parseFailedTask({
157
+ body,
158
+ ref,
159
+ statusCode,
160
+ timeoutSeconds
161
+ });
162
+ return {
163
+ data: {
164
+ createdAt: new Date(body.createTime),
165
+ ref,
166
+ state: body.state,
167
+ timeoutSeconds,
168
+ updatedAt: new Date(body.updateTime),
169
+ user: body.user
170
+ },
171
+ success: true
172
+ };
173
+ }
174
+ function isAcceptedWireState(state) {
175
+ return state === "QUEUED" || state === "PROCESSING" || state === "CANCELLED" || state === "COMPLETE" || state === "FAILED";
176
+ }
177
+ function isErrorWireCode(code) {
178
+ return code === "SCRIPT_ERROR" || code === "DEADLINE_EXCEEDED" || code === "OUTPUT_SIZE_LIMIT_EXCEEDED" || code === "INTERNAL_ERROR";
179
+ }
180
+ function isErrorWire(value) {
181
+ return isRecord(value) && isErrorWireCode(value["code"]) && typeof value["message"] === "string";
182
+ }
183
+ function isOptionalErrorWire(value) {
184
+ return value === void 0 || isErrorWire(value);
185
+ }
186
+ function isOutputWire(value) {
187
+ return isRecord(value) && Array.isArray(value["results"]);
188
+ }
189
+ function isOptionalOutputWire(value) {
190
+ return value === void 0 || isOutputWire(value);
191
+ }
192
+ function isLuauExecutionTaskWire(body) {
193
+ return isRecord(body) && typeof body["path"] === "string" && typeof body["createTime"] === "string" && typeof body["updateTime"] === "string" && isAcceptedWireState(body["state"]) && typeof body["user"] === "string" && isOptionalOutputWire(body["output"]) && isOptionalErrorWire(body["error"]) && isOptionalDurationWire(body["timeout"]);
194
+ }
195
+ const DURATION_PATTERN = /^(\d+)s$/;
196
+ function isOptionalDurationWire(value) {
197
+ return value === void 0 || typeof value === "string" && DURATION_PATTERN.test(value);
198
+ }
199
+ function parseTimeoutSeconds(value) {
200
+ if (value === void 0) return;
201
+ const seconds = DURATION_PATTERN.exec(value)?.[1];
202
+ if (seconds === void 0) return;
203
+ return Number.parseInt(seconds, 10);
204
+ }
205
+ function malformed(statusCode) {
206
+ return {
207
+ err: new ApiError(MALFORMED_TASK_MESSAGE, { statusCode }),
208
+ success: false
209
+ };
210
+ }
211
+ function parseCompleteTask(args) {
212
+ const { body, ref, statusCode, timeoutSeconds } = args;
213
+ if (body.output === void 0) return malformed(statusCode);
214
+ return {
215
+ data: {
216
+ createdAt: new Date(body.createTime),
217
+ output: { results: body.output.results },
218
+ ref,
219
+ state: "COMPLETE",
220
+ timeoutSeconds,
221
+ updatedAt: new Date(body.updateTime),
222
+ user: body.user
223
+ },
224
+ success: true
225
+ };
226
+ }
227
+ function parseFailedTask(args) {
228
+ const { body, ref, statusCode, timeoutSeconds } = args;
229
+ if (body.error === void 0) return malformed(statusCode);
230
+ return {
231
+ data: {
232
+ createdAt: new Date(body.createTime),
233
+ error: {
234
+ code: body.error.code,
235
+ message: body.error.message
236
+ },
237
+ ref,
238
+ state: "FAILED",
239
+ timeoutSeconds,
240
+ updatedAt: new Date(body.updateTime),
241
+ user: body.user
242
+ },
243
+ success: true
244
+ };
245
+ }
246
+ function parseTaskRef(path) {
247
+ const match = PATH_PATTERN.exec(path);
248
+ if (match === null) return;
249
+ const [, universeId, placeId, versionId, sessionId, sessionTaskId, plainTaskId] = match;
250
+ const taskId = sessionTaskId ?? plainTaskId;
251
+ if (universeId === void 0 || placeId === void 0 || taskId === void 0) return;
252
+ return {
253
+ placeId,
254
+ sessionId,
255
+ taskId,
256
+ universeId,
257
+ versionId
258
+ };
259
+ }
260
+ //#endregion
261
+ //#region src/domains/cloud-v2/luau-execution-tasks/specs.ts
262
+ function makeSpec(spec) {
263
+ return Object.freeze(spec);
264
+ }
265
+ /**
266
+ * Per-method dispatch spec for submitting a Luau execution task at a
267
+ * place's head version. Frozen at module scope so both the top-level
268
+ * `LuauExecutionClient` and the `luauExecution` Operation Group on
269
+ * `PlacesClient` share the same instance reference.
270
+ */
271
+ const SUBMIT_HEAD_SPEC = makeSpec({
272
+ buildRequest: (parameters) => okRequest(buildSubmitAtHeadRequest(parameters)),
273
+ methodDefaults: CREATE_METHOD_DEFAULTS,
274
+ methodKind: "create",
275
+ operationLimit: SUBMIT_OPERATION_LIMIT,
276
+ parse: parseLuauExecutionTaskResponse,
277
+ requiredScopes: SUBMIT_REQUIRED_SCOPES
278
+ });
279
+ /**
280
+ * Per-method dispatch spec for submitting a Luau execution task at a
281
+ * specific place version. Shares the rate-limit queue and required
282
+ * scope set with {@link SUBMIT_HEAD_SPEC} because Roblox attributes
283
+ * both URL shapes to one per-minute quota.
284
+ */
285
+ const SUBMIT_VERSION_SPEC = makeSpec({
286
+ buildRequest: (parameters) => okRequest(buildSubmitAtVersionRequest(parameters)),
287
+ methodDefaults: CREATE_METHOD_DEFAULTS,
288
+ methodKind: "create",
289
+ operationLimit: SUBMIT_OPERATION_LIMIT,
290
+ parse: parseLuauExecutionTaskResponse,
291
+ requiredScopes: SUBMIT_REQUIRED_SCOPES
292
+ });
293
+ /**
294
+ * Per-method dispatch spec for fetching a Luau execution task. Uses
295
+ * idempotent retry semantics (429 and 5xx both retried) so reads
296
+ * recover transparently from transient server errors.
297
+ */
298
+ const GET_SPEC = makeSpec({
299
+ buildRequest: buildGetRequest,
300
+ methodDefaults: IDEMPOTENT_METHOD_DEFAULTS,
301
+ methodKind: "idempotent",
302
+ operationLimit: GET_OPERATION_LIMIT,
303
+ parse: parseLuauExecutionTaskResponse,
304
+ requiredScopes: GET_REQUIRED_SCOPES
305
+ });
306
+ //#endregion
307
+ export { SUBMIT_HEAD_SPEC as n, SUBMIT_VERSION_SPEC as r, GET_SPEC as t };
308
+
309
+ //# sourceMappingURL=specs-Co6qYp_E.mjs.map