bsuir-iis-api 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -177,25 +177,7 @@ function isAbortError(error) {
177
177
  return false;
178
178
  }
179
179
 
180
- // src/client/http.ts
181
- var RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
182
- function buildUrl(baseUrl, path, query) {
183
- const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
184
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
185
- const url = new URL(`${normalizedBase}${normalizedPath}`);
186
- if (query) {
187
- for (const [key, value] of Object.entries(query)) {
188
- if (value === void 0 || value === null) {
189
- continue;
190
- }
191
- url.searchParams.set(key, String(value));
192
- }
193
- }
194
- return url.toString();
195
- }
196
- function sleep(ms) {
197
- return new Promise((resolve) => setTimeout(resolve, ms));
198
- }
180
+ // src/client/http/cache.ts
199
181
  function tryReadCache(config, key) {
200
182
  const entry = config.responseCache.get(key);
201
183
  if (!entry) {
@@ -240,14 +222,81 @@ function setCache(config, key, value) {
240
222
  }
241
223
  }
242
224
  }
243
- function combineAbortSignals(first, second) {
244
- if (!first) {
245
- return second;
225
+
226
+ // src/client/http/response.ts
227
+ async function readBodyTextWithLimit(response, maxResponseBytes) {
228
+ const contentLengthHeader = response.headers.get("content-length");
229
+ if (contentLengthHeader) {
230
+ const parsedLength = Number(contentLengthHeader);
231
+ if (Number.isFinite(parsedLength) && parsedLength > maxResponseBytes) {
232
+ throw new BsuirApiError(
233
+ `Response body exceeds maxResponseBytes limit (${String(maxResponseBytes)} bytes)`,
234
+ response.status,
235
+ response.url,
236
+ null
237
+ );
238
+ }
246
239
  }
247
- if (!second) {
248
- return first;
240
+ if (!response.body || typeof response.body.getReader !== "function") {
241
+ const fallbackText = await response.text();
242
+ if (new TextEncoder().encode(fallbackText).byteLength > maxResponseBytes) {
243
+ throw new BsuirApiError(
244
+ `Response body exceeds maxResponseBytes limit (${String(maxResponseBytes)} bytes)`,
245
+ response.status,
246
+ response.url,
247
+ null
248
+ );
249
+ }
250
+ return fallbackText;
251
+ }
252
+ const reader = response.body.getReader();
253
+ const decoder = new TextDecoder();
254
+ let bytesRead = 0;
255
+ let text = "";
256
+ for (; ; ) {
257
+ const { done, value } = await reader.read();
258
+ if (done) {
259
+ break;
260
+ }
261
+ bytesRead += value.byteLength;
262
+ if (bytesRead > maxResponseBytes) {
263
+ await reader.cancel();
264
+ throw new BsuirApiError(
265
+ `Response body exceeds maxResponseBytes limit (${String(maxResponseBytes)} bytes)`,
266
+ response.status,
267
+ response.url,
268
+ null
269
+ );
270
+ }
271
+ text += decoder.decode(value, { stream: true });
249
272
  }
250
- return mergeSignals([first, second]);
273
+ text += decoder.decode();
274
+ return text;
275
+ }
276
+ async function parseBody(response, maxResponseBytes) {
277
+ const contentType = response.headers.get("content-type") ?? "";
278
+ const declaredJson = contentType.includes("application/json");
279
+ const text = await readBodyTextWithLimit(response, maxResponseBytes);
280
+ if (text.length === 0) {
281
+ if (declaredJson) {
282
+ throw new BsuirApiError("Invalid JSON response payload", response.status, response.url, null);
283
+ }
284
+ return "";
285
+ }
286
+ try {
287
+ return JSON.parse(text);
288
+ } catch {
289
+ if (declaredJson) {
290
+ throw new BsuirApiError("Invalid JSON response payload", response.status, response.url, null);
291
+ }
292
+ return text;
293
+ }
294
+ }
295
+
296
+ // src/client/http/retry.ts
297
+ var RETRIABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
298
+ function sleep(ms) {
299
+ return new Promise((resolve) => setTimeout(resolve, ms));
251
300
  }
252
301
  function parseRetryAfterMs(retryAfter) {
253
302
  if (!retryAfter || retryAfter.trim().length === 0) {
@@ -281,24 +330,32 @@ function getRetryDelayMs(config, attempt, retryAfterHeader) {
281
330
  const jitterFactor = 0.75 + Math.random() * 0.5;
282
331
  return Math.floor(baseDelay * jitterFactor);
283
332
  }
284
- async function parseBody(response) {
285
- const contentType = response.headers.get("content-type") ?? "";
286
- const declaredJson = contentType.includes("application/json");
287
- const text = await response.text();
288
- if (text.length === 0) {
289
- if (declaredJson) {
290
- throw new BsuirApiError("Invalid JSON response payload", response.status, response.url, null);
333
+
334
+ // src/client/http/url.ts
335
+ function buildUrl(baseUrl, path, query) {
336
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
337
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
338
+ const url = new URL(`${normalizedBase}${normalizedPath}`);
339
+ if (query) {
340
+ for (const [key, value] of Object.entries(query)) {
341
+ if (value === void 0 || value === null) {
342
+ continue;
343
+ }
344
+ url.searchParams.set(key, String(value));
291
345
  }
292
- return "";
293
346
  }
294
- try {
295
- return JSON.parse(text);
296
- } catch {
297
- if (declaredJson) {
298
- throw new BsuirApiError("Invalid JSON response payload", response.status, response.url, null);
299
- }
300
- return text;
347
+ return url.toString();
348
+ }
349
+
350
+ // src/client/http/requestJson.ts
351
+ function combineAbortSignals(first, second) {
352
+ if (!first) {
353
+ return second;
354
+ }
355
+ if (!second) {
356
+ return first;
301
357
  }
358
+ return mergeSignals([first, second]);
302
359
  }
303
360
  function baseHookContext(method, path, endpoint, attempt, maxAttempts, query) {
304
361
  return {
@@ -367,7 +424,7 @@ async function requestJson(config, path, options = {}) {
367
424
  }
368
425
  const response = await config.fetchImpl(endpoint, requestInit);
369
426
  if (!response.ok) {
370
- const errorBody = await parseBody(response);
427
+ const errorBody = await parseBody(response, config.maxResponseBytes);
371
428
  if (attempt < maxRetries && RETRIABLE_STATUS_CODES.has(response.status)) {
372
429
  const delayMs = getRetryDelayMs(config, attempt, response.headers.get("retry-after"));
373
430
  const retryCtx = {
@@ -394,7 +451,7 @@ async function requestJson(config, path, options = {}) {
394
451
  config.hooks.onError?.(errorCtx);
395
452
  throw apiError;
396
453
  }
397
- const parsed = await parseBody(response);
454
+ const parsed = await parseBody(response, config.maxResponseBytes);
398
455
  const responseCtx = {
399
456
  ...hookCtx,
400
457
  status: response.status,
@@ -596,119 +653,47 @@ function createAnnouncementsModule(config) {
596
653
  };
597
654
  }
598
655
 
599
- // src/modules/auditories.ts
600
- function createAuditoriesModule(config) {
656
+ // src/modules/createListModule.ts
657
+ function createListModule(config, endpoint) {
601
658
  return {
602
659
  /**
603
- * Returns the full list of auditories from `/auditories`.
604
- * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
605
- *
606
- * @throws {BsuirApiError} When the API returns a non-success HTTP status
607
- * @throws {BsuirNetworkError} On transport failures after retries
608
- * @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
609
- */
660
+ * Returns all items from the configured endpoint.
661
+ */
610
662
  async listAll(options = {}) {
611
- const payload = await requestJson(config, "/auditories", {
663
+ const payload = await requestJson(config, endpoint, {
612
664
  signal: options.signal
613
665
  });
614
666
  if (config.validateResponses) {
615
- assertArrayResponse(payload, "/auditories");
667
+ assertArrayResponse(payload, endpoint);
616
668
  }
617
669
  return payload;
618
670
  }
619
671
  };
620
672
  }
621
673
 
674
+ // src/modules/auditories.ts
675
+ function createAuditoriesModule(config) {
676
+ return createListModule(config, "/auditories");
677
+ }
678
+
622
679
  // src/modules/departments.ts
623
680
  function createDepartmentsModule(config) {
624
- return {
625
- /**
626
- * Returns the full list of departments from `/departments`.
627
- * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
628
- *
629
- * @throws {BsuirApiError} When the API returns a non-success HTTP status
630
- * @throws {BsuirNetworkError} On transport failures after retries
631
- * @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
632
- */
633
- async listAll(options = {}) {
634
- const payload = await requestJson(config, "/departments", {
635
- signal: options.signal
636
- });
637
- if (config.validateResponses) {
638
- assertArrayResponse(payload, "/departments");
639
- }
640
- return payload;
641
- }
642
- };
681
+ return createListModule(config, "/departments");
643
682
  }
644
683
 
645
684
  // src/modules/employees.ts
646
685
  function createEmployeesModule(config) {
647
- return {
648
- /**
649
- * Returns the full list of employees from `/employees/all`.
650
- * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
651
- *
652
- * @throws {BsuirApiError} When the API returns a non-success HTTP status
653
- * @throws {BsuirNetworkError} On transport failures after retries
654
- * @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
655
- */
656
- async listAll(options = {}) {
657
- const payload = await requestJson(config, "/employees/all", {
658
- signal: options.signal
659
- });
660
- if (config.validateResponses) {
661
- assertArrayResponse(payload, "/employees/all");
662
- }
663
- return payload;
664
- }
665
- };
686
+ return createListModule(config, "/employees/all");
666
687
  }
667
688
 
668
689
  // src/modules/faculties.ts
669
690
  function createFacultiesModule(config) {
670
- return {
671
- /**
672
- * Returns the full list of faculties from `/faculties`.
673
- * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
674
- *
675
- * @throws {BsuirApiError} When the API returns a non-success HTTP status
676
- * @throws {BsuirNetworkError} On transport failures after retries
677
- * @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
678
- */
679
- async listAll(options = {}) {
680
- const payload = await requestJson(config, "/faculties", {
681
- signal: options.signal
682
- });
683
- if (config.validateResponses) {
684
- assertArrayResponse(payload, "/faculties");
685
- }
686
- return payload;
687
- }
688
- };
691
+ return createListModule(config, "/faculties");
689
692
  }
690
693
 
691
694
  // src/modules/groups.ts
692
695
  function createGroupsModule(config) {
693
- return {
694
- /**
695
- * Returns the full list of student groups from `/student-groups`.
696
- * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
697
- *
698
- * @throws {BsuirApiError} When the API returns a non-success HTTP status
699
- * @throws {BsuirNetworkError} On transport failures after retries
700
- * @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
701
- */
702
- async listAll(options = {}) {
703
- const payload = await requestJson(config, "/student-groups", {
704
- signal: options.signal
705
- });
706
- if (config.validateResponses) {
707
- assertArrayResponse(payload, "/student-groups");
708
- }
709
- return payload;
710
- }
711
- };
696
+ return createListModule(config, "/student-groups");
712
697
  }
713
698
 
714
699
  // src/utils/week.ts
@@ -747,7 +732,58 @@ function parseCurrentWeekInternal(payload, depth) {
747
732
  throw new BsuirValidationError("'currentWeek' response payload must be a positive integer");
748
733
  }
749
734
 
750
- // src/modules/schedule.ts
735
+ // src/modules/scheduleFilter.ts
736
+ function lessonAuditories(item) {
737
+ const { auditories } = item;
738
+ return Array.isArray(auditories) ? auditories : [];
739
+ }
740
+ function matchesFilter(item, filter) {
741
+ if (filter.source && item.source !== filter.source) {
742
+ return false;
743
+ }
744
+ if (filter.weekday && item.day !== filter.weekday) {
745
+ return false;
746
+ }
747
+ if (typeof filter.weekNumber === "number") {
748
+ if (!Array.isArray(item.weekNumber) || !item.weekNumber.includes(filter.weekNumber)) {
749
+ return false;
750
+ }
751
+ }
752
+ if (typeof filter.subgroup === "number" && item.numSubgroup !== filter.subgroup) {
753
+ return false;
754
+ }
755
+ if (filter.lessonTypeAbbrev) {
756
+ const types = Array.isArray(filter.lessonTypeAbbrev) ? filter.lessonTypeAbbrev : [filter.lessonTypeAbbrev];
757
+ if (!item.lessonTypeAbbrev || !types.includes(item.lessonTypeAbbrev)) {
758
+ return false;
759
+ }
760
+ }
761
+ if (filter.subjectQuery) {
762
+ const query = filter.subjectQuery.toLowerCase();
763
+ const haystack = `${item.subject} ${item.subjectFullName} ${item.note ?? ""}`.toLowerCase();
764
+ if (!haystack.includes(query)) {
765
+ return false;
766
+ }
767
+ }
768
+ if (filter.employeeUrlId) {
769
+ const employeeMatch = item.employees?.some((employee) => employee.urlId === filter.employeeUrlId);
770
+ if (!employeeMatch) {
771
+ return false;
772
+ }
773
+ }
774
+ if (filter.auditory && !lessonAuditories(item).includes(filter.auditory)) {
775
+ return false;
776
+ }
777
+ return true;
778
+ }
779
+ function filterLessons(response, filter) {
780
+ if (typeof filter.weekNumber === "number") {
781
+ assertPositiveInt(filter.weekNumber, "filter.weekNumber");
782
+ }
783
+ return response.lessons.filter((item) => matchesFilter(item, filter));
784
+ }
785
+
786
+ // src/modules/scheduleNormalize.ts
751
787
  var WEEKDAYS = [
752
788
  "\u041F\u043E\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u0438\u043A",
753
789
  "\u0412\u0442\u043E\u0440\u043D\u0438\u043A",
@@ -756,7 +792,7 @@ var WEEKDAYS = [
756
792
  "\u041F\u044F\u0442\u043D\u0438\u0446\u0430",
757
793
  "\u0421\u0443\u0431\u0431\u043E\u0442\u0430"
758
794
  ];
759
- function lessonAuditories(item) {
795
+ function lessonAuditories2(item) {
760
796
  const { auditories } = item;
761
797
  return Array.isArray(auditories) ? auditories : [];
762
798
  }
@@ -772,7 +808,7 @@ function normalizeSchedule(response) {
772
808
  for (const day of WEEKDAYS) {
773
809
  const dayItems = safeSchedules[day] ?? [];
774
810
  const flattenedDayItems = dayItems.map((item) => {
775
- const auditories = lessonAuditories(item);
811
+ const auditories = lessonAuditories2(item);
776
812
  return {
777
813
  ...item,
778
814
  auditories,
@@ -785,12 +821,11 @@ function normalizeSchedule(response) {
785
821
  scheduleLessons.push(...flattenedDayItems);
786
822
  }
787
823
  for (const exam of safeExams) {
788
- const auditories = lessonAuditories(exam);
824
+ const auditories = lessonAuditories2(exam);
789
825
  const flattenedExam = {
790
826
  ...exam,
791
827
  auditories,
792
828
  day: null,
793
- // Exams are not grouped by weekday in API response.
794
829
  source: "exams"
795
830
  };
796
831
  lessons.push(flattenedExam);
@@ -806,51 +841,8 @@ function normalizeSchedule(response) {
806
841
  examLessons
807
842
  };
808
843
  }
809
- function matchesFilter(item, filter) {
810
- if (filter.source && item.source !== filter.source) {
811
- return false;
812
- }
813
- if (filter.weekday && item.day !== filter.weekday) {
814
- return false;
815
- }
816
- if (typeof filter.weekNumber === "number") {
817
- if (!Array.isArray(item.weekNumber) || !item.weekNumber.includes(filter.weekNumber)) {
818
- return false;
819
- }
820
- }
821
- if (typeof filter.subgroup === "number" && item.numSubgroup !== filter.subgroup) {
822
- return false;
823
- }
824
- if (filter.lessonTypeAbbrev) {
825
- const types = Array.isArray(filter.lessonTypeAbbrev) ? filter.lessonTypeAbbrev : [filter.lessonTypeAbbrev];
826
- if (!item.lessonTypeAbbrev || !types.includes(item.lessonTypeAbbrev)) {
827
- return false;
828
- }
829
- }
830
- if (filter.subjectQuery) {
831
- const query = filter.subjectQuery.toLowerCase();
832
- const haystack = `${item.subject} ${item.subjectFullName} ${item.note ?? ""}`.toLowerCase();
833
- if (!haystack.includes(query)) {
834
- return false;
835
- }
836
- }
837
- if (filter.employeeUrlId) {
838
- const employeeMatch = item.employees?.some((employee) => employee.urlId === filter.employeeUrlId);
839
- if (!employeeMatch) {
840
- return false;
841
- }
842
- }
843
- if (filter.auditory && !lessonAuditories(item).includes(filter.auditory)) {
844
- return false;
845
- }
846
- return true;
847
- }
848
- function filterLessons(response, filter) {
849
- if (typeof filter.weekNumber === "number") {
850
- assertPositiveInt(filter.weekNumber, "filter.weekNumber");
851
- }
852
- return response.lessons.filter((item) => matchesFilter(item, filter));
853
- }
844
+
845
+ // src/modules/scheduleApi.ts
854
846
  function createScheduleModule(config) {
855
847
  async function getGroup(groupNumber, options = {}) {
856
848
  assertGroupNumber(groupNumber, "groupNumber");
@@ -895,24 +887,35 @@ function createScheduleModule(config) {
895
887
  getEmployee,
896
888
  getGroupFiltered,
897
889
  getEmployeeFiltered,
890
+ /**
891
+ * Returns exams for a group.
892
+ */
898
893
  async getGroupExams(groupNumber, options = {}) {
899
894
  return getGroupFiltered(groupNumber, { source: "exams" }, options);
900
895
  },
896
+ /**
897
+ * Returns exams for an employee.
898
+ */
901
899
  async getEmployeeExams(urlId, options = {}) {
902
900
  return getEmployeeFiltered(urlId, { source: "exams" }, options);
903
901
  },
902
+ /**
903
+ * Returns regular schedule lessons of a specific subgroup for a group.
904
+ */
904
905
  async getGroupBySubgroup(groupNumber, subgroup, options = {}) {
905
906
  assertPositiveInt(subgroup, "subgroup");
906
907
  return getGroupFiltered(groupNumber, { source: "schedules", subgroup }, options);
907
908
  },
909
+ /**
910
+ * Returns regular schedule lessons of a specific subgroup for an employee.
911
+ */
908
912
  async getEmployeeBySubgroup(urlId, subgroup, options = {}) {
909
913
  assertPositiveInt(subgroup, "subgroup");
910
914
  return getEmployeeFiltered(urlId, { source: "schedules", subgroup }, options);
911
915
  },
912
916
  getCurrentWeek,
913
917
  /**
914
- * Calls IIS `/last-update-date/student-group`. That route is legacy and unsupported on the server;
915
- * it may return an error for newer group numbers (e.g. six-digit `524404`).
918
+ * Calls IIS `/last-update-date/student-group`.
916
919
  */
917
920
  async getLastUpdateByGroup(params, options = {}) {
918
921
  let query;
@@ -933,8 +936,7 @@ function createScheduleModule(config) {
933
936
  return payload;
934
937
  },
935
938
  /**
936
- * Calls IIS `/last-update-date/employee`. That route is legacy and unsupported on the server; prefer
937
- * not relying on it for critical cache logic.
939
+ * Calls IIS `/last-update-date/employee`.
938
940
  */
939
941
  async getLastUpdateByEmployee(params, options = {}) {
940
942
  let query;
@@ -959,29 +961,13 @@ function createScheduleModule(config) {
959
961
 
960
962
  // src/modules/specialities.ts
961
963
  function createSpecialitiesModule(config) {
962
- return {
963
- /**
964
- * Returns the full list of specialities from `/specialities`.
965
- * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
966
- *
967
- * @throws {BsuirApiError} When the API returns a non-success HTTP status
968
- * @throws {BsuirNetworkError} On transport failures after retries
969
- * @throws {BsuirTimeoutError} When the request exceeds `timeoutMs`
970
- */
971
- async listAll(options = {}) {
972
- const payload = await requestJson(config, "/specialities", {
973
- signal: options.signal
974
- });
975
- if (config.validateResponses) {
976
- assertArrayResponse(payload, "/specialities");
977
- }
978
- return payload;
979
- }
980
- };
964
+ return createListModule(config, "/specialities");
981
965
  }
982
966
 
983
967
  // src/client/createClient.ts
984
968
  var DEFAULT_BASE_URL = "https://iis.bsuir.by/api/v1";
969
+ var DEFAULT_ALLOWED_BASE_URL_HOSTS = ["iis.bsuir.by"];
970
+ var DEFAULT_MAX_RESPONSE_BYTES = 5e6;
985
971
  var MAX_TIMEOUT_MS = 3e5;
986
972
  function resolveFetch(customFetch) {
987
973
  if (customFetch) {
@@ -1005,6 +991,39 @@ function assertIntegerOption(value, name, minInclusive) {
1005
991
  }
1006
992
  return value;
1007
993
  }
994
+ function normalizeBaseUrl(rawBaseUrl, allowInsecureHttp, allowedHosts) {
995
+ let parsed;
996
+ try {
997
+ parsed = new URL(rawBaseUrl);
998
+ } catch {
999
+ throw new BsuirConfigurationError("'baseUrl' must be a valid absolute URL");
1000
+ }
1001
+ if (parsed.username || parsed.password) {
1002
+ throw new BsuirConfigurationError("'baseUrl' must not include credentials");
1003
+ }
1004
+ if (parsed.search || parsed.hash) {
1005
+ throw new BsuirConfigurationError("'baseUrl' must not include query string or hash");
1006
+ }
1007
+ if (parsed.protocol !== "https:" && !(allowInsecureHttp && parsed.protocol === "http:")) {
1008
+ throw new BsuirConfigurationError(
1009
+ "'baseUrl' must use HTTPS. Set 'allowInsecureHttp: true' only for trusted local/test endpoints."
1010
+ );
1011
+ }
1012
+ const normalizedAllowedHosts = new Set(
1013
+ allowedHosts.map((host2) => host2.trim().toLowerCase()).filter((host2) => host2.length > 0)
1014
+ );
1015
+ if (normalizedAllowedHosts.size === 0) {
1016
+ throw new BsuirConfigurationError("'allowedBaseUrlHosts' must contain at least one hostname");
1017
+ }
1018
+ const host = parsed.hostname.toLowerCase();
1019
+ if (!normalizedAllowedHosts.has(host)) {
1020
+ throw new BsuirConfigurationError(
1021
+ `'baseUrl' host '${host}' is not allowed. Allowed hosts: ${[...normalizedAllowedHosts].join(", ")}`
1022
+ );
1023
+ }
1024
+ const normalizedPath = parsed.pathname.replace(/\/+$/, "");
1025
+ return `${parsed.origin}${normalizedPath}`;
1026
+ }
1008
1027
  function createInternalConfig(options) {
1009
1028
  const timeoutMs = assertIntegerOption(options.timeoutMs, "timeoutMs", 1) ?? 1e4;
1010
1029
  if (timeoutMs > MAX_TIMEOUT_MS) {
@@ -1017,13 +1036,16 @@ function createInternalConfig(options) {
1017
1036
  const retryMaxDelayMs = assertIntegerOption(options.retryMaxDelayMs, "retryMaxDelayMs", 0) ?? 3e3;
1018
1037
  const cacheTtlMs = assertIntegerOption(options.cache?.ttlMs, "cache.ttlMs", 1);
1019
1038
  const cacheMaxEntries = assertIntegerOption(options.cache?.maxEntries, "cache.maxEntries", 1) ?? 200;
1039
+ const maxResponseBytes = assertIntegerOption(options.maxResponseBytes, "maxResponseBytes", 1) ?? DEFAULT_MAX_RESPONSE_BYTES;
1040
+ const allowInsecureHttp = options.allowInsecureHttp ?? false;
1041
+ const allowedBaseUrlHosts = options.allowedBaseUrlHosts ?? DEFAULT_ALLOWED_BASE_URL_HOSTS;
1020
1042
  if (retryDelayMs > retryMaxDelayMs) {
1021
1043
  throw new BsuirConfigurationError(
1022
1044
  "'retryDelayMs' must be less than or equal to 'retryMaxDelayMs'"
1023
1045
  );
1024
1046
  }
1025
1047
  return {
1026
- baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
1048
+ baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL, allowInsecureHttp, allowedBaseUrlHosts),
1027
1049
  fetchImpl: resolveFetch(options.fetch),
1028
1050
  signal: options.signal,
1029
1051
  timeoutMs,
@@ -1035,7 +1057,8 @@ function createInternalConfig(options) {
1035
1057
  cacheTtlMs,
1036
1058
  cacheMaxEntries,
1037
1059
  dedupeInFlight: options.dedupeInFlight ?? true,
1038
- validateResponses: options.validateResponses ?? false,
1060
+ maxResponseBytes,
1061
+ validateResponses: options.validateResponses ?? true,
1039
1062
  hooks: options.hooks ?? {},
1040
1063
  responseCache: /* @__PURE__ */ new Map(),
1041
1064
  inFlightRequests: /* @__PURE__ */ new Map(),