@thallylabs/migrate 0.2.25 → 0.2.26

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
@@ -889,27 +889,104 @@ function normalizeAssetPath(value) {
889
889
  function objectValue(value) {
890
890
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
891
891
  }
892
+ var OPEN_NAVIGATION_ACCESS = Object.freeze({ hasPublicFalse: false, hasPublicTrue: false });
892
893
  var PUBLIC_FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "no", "off", "0"]);
893
- function isPublicFalse(value) {
894
- return value === false || value === 0 || typeof value === "string" && PUBLIC_FALSE_STRINGS.has(value.trim().toLowerCase());
894
+ var PUBLIC_TRUE_STRINGS = /* @__PURE__ */ new Set(["true"]);
895
+ function parsePublic(value) {
896
+ if (value === void 0 || value === null) return { isMalformed: false };
897
+ if (value === true) return { isPublic: true, isMalformed: false };
898
+ if (value === false || value === 0) return { isPublic: false, isMalformed: false };
899
+ if (typeof value === "string") {
900
+ const normalized = value.trim().toLowerCase();
901
+ if (PUBLIC_TRUE_STRINGS.has(normalized)) return { isPublic: true, isMalformed: false };
902
+ if (PUBLIC_FALSE_STRINGS.has(normalized)) return { isPublic: false, isMalformed: false };
903
+ }
904
+ return { isMalformed: true };
905
+ }
906
+ function parseGroups(value) {
907
+ if (value === void 0 || value === null || value === "" || value === false) return { groups: [], isMalformed: false };
908
+ if (typeof value === "string") {
909
+ const groups = value.split(",").map((group) => group.trim()).filter(Boolean);
910
+ return { groups: [...new Set(groups)], isMalformed: groups.length === 0 };
911
+ }
912
+ if (Array.isArray(value)) {
913
+ if (value.some((group) => typeof group !== "string")) return { groups: [], isMalformed: true };
914
+ const groups = value.map((group) => group.trim()).filter(Boolean);
915
+ return { groups: [...new Set(groups)], isMalformed: value.length > 0 && groups.length === 0 };
916
+ }
917
+ return { groups: [], isMalformed: true };
918
+ }
919
+ function accessFrom(groupsValue, publicValue, where) {
920
+ const groups = parseGroups(groupsValue);
921
+ const visibility = parsePublic(publicValue);
922
+ const problem = groups.isMalformed ? `${where} \`groups\` is not a list of group names` : visibility.isMalformed ? `${where} \`public\` is neither true nor false` : void 0;
923
+ return {
924
+ ...groups.groups.length > 0 ? { groups: groups.groups } : {},
925
+ ...visibility.isPublic !== void 0 ? { isPublic: visibility.isPublic } : {},
926
+ ...problem ? { problem } : {}
927
+ };
895
928
  }
896
- function isPublicTrue(value) {
897
- return value === true || typeof value === "string" && value.trim().toLowerCase() === "true";
929
+ function frontmatterAccess(data) {
930
+ return accessFrom(data.groups, data.public, "frontmatter");
898
931
  }
899
- function frontmatterGateReason(data) {
900
- const groups = data.groups;
901
- const hasGroups = Array.isArray(groups) ? groups.length > 0 : typeof groups === "string" ? groups.trim() !== "" : groups !== void 0 && groups !== null && groups !== false;
902
- if (hasGroups) return "frontmatter `groups` restricts it to authenticated groups";
903
- if (isPublicFalse(data.public)) return "frontmatter `public: false`";
904
- return void 0;
932
+ function isRestrictedAccess(access) {
933
+ return Boolean(access.problem) || (access.groups?.length ?? 0) > 0 || access.isPublic === false;
934
+ }
935
+ var NAVIGATION_CONTAINER_KEYS = ["group", "pages", "tab", "groups", "page", "root", "menu", "href"];
936
+ function navigationAccess(node) {
937
+ const isContainer = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && NAVIGATION_CONTAINER_KEYS.some((key) => key in entry);
938
+ const groups = Array.isArray(node.groups) && node.groups.length > 0 && node.groups.every(isContainer) ? void 0 : node.groups;
939
+ const access = accessFrom(groups, node.public, "its navigation container's");
940
+ return Object.keys(access).length > 0 ? access : void 0;
905
941
  }
906
942
  function navigationGateReason(node) {
907
- const groups = node.groups;
908
- const isContainer = (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) && ["group", "pages", "tab", "groups", "page", "root", "menu", "href"].some((key) => key in entry);
909
- const restricted = typeof groups === "string" ? groups.trim() !== "" : Array.isArray(groups) && groups.length > 0 && !groups.every(isContainer);
910
- if (restricted) return "its navigation container sets `groups`";
911
- if (isPublicFalse(node.public)) return "its navigation container sets `public: false`";
912
- return void 0;
943
+ const access = navigationAccess(node);
944
+ if (!access || !isRestrictedAccess(access)) return void 0;
945
+ return access.problem ?? (access.groups ? "its navigation container sets `groups`" : "its navigation container sets `public: false`");
946
+ }
947
+ function intersect(left, right) {
948
+ if (!left) return right;
949
+ if (!right) return left;
950
+ return left.filter((group) => right.includes(group));
951
+ }
952
+ function descendNavigationAccess(outer, container) {
953
+ if (!container) return outer;
954
+ const groups = intersect(outer.groups, container.groups);
955
+ const problem = outer.problem ?? container.problem ?? (groups && groups.length === 0 ? "nested navigation containers set `groups` that share no group" : void 0);
956
+ return {
957
+ ...groups ? { groups } : {},
958
+ hasPublicFalse: outer.hasPublicFalse || container.isPublic === false,
959
+ hasPublicTrue: outer.hasPublicTrue || container.isPublic === true,
960
+ ...problem ? { problem } : {}
961
+ };
962
+ }
963
+ function mergeNavigationAppearances(appearances) {
964
+ if (appearances.length === 0) return OPEN_NAVIGATION_ACCESS;
965
+ let groups;
966
+ for (const appearance of appearances) groups = intersect(groups, appearance.groups);
967
+ const problem = appearances.find((appearance) => appearance.problem)?.problem ?? (groups && groups.length === 0 ? "it is listed under navigation containers whose `groups` share no group" : void 0);
968
+ return {
969
+ ...groups ? { groups } : {},
970
+ hasPublicFalse: appearances.some((appearance) => appearance.hasPublicFalse),
971
+ hasPublicTrue: appearances.every((appearance) => appearance.hasPublicTrue),
972
+ ...problem ? { problem } : {}
973
+ };
974
+ }
975
+ function hasNavigationRules(access) {
976
+ return isNavigationRestricted(access) || access.hasPublicTrue;
977
+ }
978
+ function isNavigationRestricted(access) {
979
+ return Boolean(access.problem) || (access.groups?.length ?? 0) > 0 || access.hasPublicFalse;
980
+ }
981
+ function resolvePageAccess(own2, navigation) {
982
+ const problem = own2.problem ?? navigation.problem;
983
+ if (problem) return { problem };
984
+ const groups = intersect(own2.groups, navigation.groups);
985
+ if (groups && groups.length === 0) {
986
+ return { problem: "its frontmatter `groups` and its navigation container's `groups` share no group" };
987
+ }
988
+ const isPublic = own2.isPublic === false || navigation.hasPublicFalse ? false : own2.isPublic === true || navigation.hasPublicTrue && !groups ? true : void 0;
989
+ return { access: { ...groups ? { groups } : {}, ...isPublic !== void 0 ? { isPublic } : {} } };
913
990
  }
914
991
  var CONFIG_BASENAME = /(?:^|[.-])(?:config|rc)\.[cm]?js$|^(?:tailwind|postcss|next|eslint|prettier|babel|webpack|vite|vitest|jest|rollup|gulpfile|gruntfile|stylelint|commitlint|lint-staged|svgo|metro)[.-]/i;
915
992
  var BUILD_OUTPUT_SEGMENTS = /* @__PURE__ */ new Set(["dist", "build", "coverage"]);
@@ -4223,22 +4300,31 @@ function collectGatedSourceRefs(node, gate, out, limits, depth = 0) {
4223
4300
  if (reason && typeof object.sourceRef === "string") out.set(object, reason);
4224
4301
  for (const value of Object.values(object)) collectGatedSourceRefs(value, reason, out, limits, depth + 1);
4225
4302
  }
4226
- function collectGatedReferences(node, gate, pathPrefix, out, depth = 0) {
4303
+ function collectReferenceAccess(node, access, pathPrefix, out, depth = 0) {
4227
4304
  if (depth > SWEEP_DEPTH_LIMIT) return;
4228
4305
  if (Array.isArray(node)) {
4229
- for (const item of node) collectGatedReferences(item, gate, pathPrefix, out, depth + 1);
4306
+ for (const item of node) collectReferenceAccess(item, access, pathPrefix, out, depth + 1);
4230
4307
  return;
4231
4308
  }
4232
4309
  const object = objectValue2(node);
4233
4310
  if (!object) return;
4234
- const reason = gate ?? navigationGateReason(object);
4235
- if (reason) {
4236
- const refs = [object.page, object.root, ...Array.isArray(object.pages) ? object.pages : []];
4311
+ const nested = descendNavigationAccess(access, navigationAccess(object));
4312
+ if (hasNavigationRules(nested)) {
4313
+ const refs = [object.page, object.root, object.href, ...Array.isArray(object.pages) ? object.pages : []];
4237
4314
  for (const ref of refs) {
4238
- if (typeof ref === "string" && normalizePageRef(ref, pathPrefix)) out.push({ ref, reason });
4315
+ if (typeof ref === "string" && normalizePageRef(ref, pathPrefix)) out.push({ ref, access: nested });
4239
4316
  }
4240
4317
  }
4241
- for (const value of Object.values(object)) collectGatedReferences(value, reason, pathPrefix, out, depth + 1);
4318
+ for (const value of Object.values(object)) collectReferenceAccess(value, nested, pathPrefix, out, depth + 1);
4319
+ }
4320
+ function withContainerAccess(object, context, walk2) {
4321
+ const outer = context.access;
4322
+ context.access = descendNavigationAccess(outer, navigationAccess(object));
4323
+ try {
4324
+ return walk2();
4325
+ } finally {
4326
+ context.access = outer;
4327
+ }
4242
4328
  }
4243
4329
  function warnOnce(context, key, message) {
4244
4330
  if (context.warningKeys.has(key)) return;
@@ -4257,11 +4343,12 @@ function containerPresentation(value) {
4257
4343
  };
4258
4344
  }
4259
4345
  function registerReference(value, context) {
4260
- const withheld = context.gateReason ?? (context.limits.tooDeep ? "its navigation is too deeply nested to check access rules" : void 0);
4261
- if (withheld) {
4262
- if (normalizePageRef(value, context.pathPrefix)) context.gated.push({ ref: value, reason: withheld });
4346
+ if (context.limits.tooDeep) {
4347
+ const problem = "its navigation is too deeply nested to check access rules";
4348
+ if (normalizePageRef(value, context.pathPrefix)) context.appearances.push({ ref: value, access: { ...context.access, problem } });
4263
4349
  return null;
4264
4350
  }
4351
+ if (normalizePageRef(value, context.pathPrefix)) context.appearances.push({ ref: value, access: context.access });
4265
4352
  const localizedValue = context.locale ? mintlifyLocalizedReference(value, context.locale, context.defaultPageIds) : value;
4266
4353
  const navigationId = normalizePageRef(localizedValue, context.pathPrefix);
4267
4354
  if (!navigationId) return null;
@@ -4287,14 +4374,7 @@ function convertPage(value, context) {
4287
4374
  }
4288
4375
  const object = objectValue2(value);
4289
4376
  if (!object) return null;
4290
- const outerGate = context.gateReason;
4291
- const gate = navigationGateReason(object);
4292
- if (gate && !outerGate) context.gateReason = gate;
4293
- try {
4294
- return convertPageObject(object, context);
4295
- } finally {
4296
- context.gateReason = outerGate;
4297
- }
4377
+ return withContainerAccess(object, context, () => convertPageObject(object, context));
4298
4378
  }
4299
4379
  function gatedSourceRef(repo, context) {
4300
4380
  if (!context.gatedSourceRefs.includes(repo)) context.gatedSourceRefs.push(repo);
@@ -4311,7 +4391,6 @@ function convertPageObject(object, context) {
4311
4391
  if (href && !object.pages && !object.groups) {
4312
4392
  const page = registerReference(href, context);
4313
4393
  if (page) return page;
4314
- if (context.gateReason) return null;
4315
4394
  warnOnce(
4316
4395
  context,
4317
4396
  "external-page-link",
@@ -4320,7 +4399,7 @@ function convertPageObject(object, context) {
4320
4399
  return null;
4321
4400
  }
4322
4401
  if (typeof object.sourceRef === "string") {
4323
- if (context.gateReason || context.gatedNodes.has(object) || context.limits.tooDeep) return gatedSourceRef(object.sourceRef, context);
4402
+ if (isNavigationRestricted(context.access) || context.gatedNodes.has(object) || context.limits.tooDeep) return gatedSourceRef(object.sourceRef, context);
4324
4403
  const imported = context.resolveSourceRef?.(object.sourceRef);
4325
4404
  if (imported) return { group: object.sourceRef.split("/").pop() ?? object.sourceRef, pages: imported };
4326
4405
  const where = context.groupLabel ? `the "${context.groupLabel}" navigation group` : "the navigation";
@@ -4344,7 +4423,7 @@ function convertPageObject(object, context) {
4344
4423
  for (const page of pages) {
4345
4424
  const pageObject = objectValue2(page);
4346
4425
  const sourceRef = pageObject?.sourceRef;
4347
- const imported = typeof sourceRef === "string" && !context.gateReason && !navigationGateReason(pageObject) && !context.gatedNodes.has(pageObject) && !context.limits.tooDeep ? context.resolveSourceRef?.(sourceRef) : null;
4426
+ const imported = typeof sourceRef === "string" && !isNavigationRestricted(context.access) && !navigationGateReason(pageObject) && !context.gatedNodes.has(pageObject) && !context.limits.tooDeep ? context.resolveSourceRef?.(sourceRef) : null;
4348
4427
  if (imported) {
4349
4428
  children.push(...imported);
4350
4429
  continue;
@@ -4462,15 +4541,7 @@ function convertContainerToTabs(containerValue, context, fallbackTab, trace, dep
4462
4541
  ...object.hidden === true ? { hidden: true } : {}
4463
4542
  }];
4464
4543
  }
4465
- const outerGate = context.gateReason;
4466
- const gate = navigationGateReason(object);
4467
- if (gate && !outerGate) context.gateReason = gate;
4468
- let nested;
4469
- try {
4470
- nested = convertContainerToTabs(object, context, tab, trace, depth + 1);
4471
- } finally {
4472
- context.gateReason = outerGate;
4473
- }
4544
+ const nested = withContainerAccess(object, context, () => convertContainerToTabs(object, context, tab, trace, depth + 1));
4474
4545
  if (nested.length > 0) {
4475
4546
  if (nested.length === 1) {
4476
4547
  return [{
@@ -4844,10 +4915,11 @@ function projectMintlifyNavigation(config, options = {}) {
4844
4915
  const references = [];
4845
4916
  const seenReferences = /* @__PURE__ */ new Set();
4846
4917
  const warningKeys = /* @__PURE__ */ new Set();
4847
- const gated = [];
4918
+ const appearances = [];
4848
4919
  const gatedSourceRefs = [];
4849
4920
  const gatedNodes = /* @__PURE__ */ new Map();
4850
4921
  const navigation = objectValue2(config.navigation) ?? config;
4922
+ const rootAccess = descendNavigationAccess(OPEN_NAVIGATION_ACCESS, navigationAccess(navigation));
4851
4923
  const limits = { tooDeep: false };
4852
4924
  collectGatedSourceRefs(navigation, void 0, gatedNodes, limits);
4853
4925
  if (limits.tooDeep) {
@@ -4896,7 +4968,8 @@ function projectMintlifyNavigation(config, options = {}) {
4896
4968
  seenReferences,
4897
4969
  warnings,
4898
4970
  warningKeys,
4899
- gated,
4971
+ appearances,
4972
+ access: descendNavigationAccess(rootAccess, navigationAccess(language)),
4900
4973
  gatedSourceRefs,
4901
4974
  gatedNodes,
4902
4975
  limits
@@ -4916,7 +4989,7 @@ function projectMintlifyNavigation(config, options = {}) {
4916
4989
  }
4917
4990
  if (Object.keys(localizedNavigation).length > 0) i18n.navigation = localizedNavigation;
4918
4991
  } else {
4919
- const context = { references, seenReferences, warnings, warningKeys, gated, gatedSourceRefs, gatedNodes, limits, pathPrefix: options.pathPrefix, resolveSourceRef: options.resolveSourceRef };
4992
+ const context = { references, seenReferences, warnings, warningKeys, appearances, access: rootAccess, gatedSourceRefs, gatedNodes, limits, pathPrefix: options.pathPrefix, resolveSourceRef: options.resolveSourceRef };
4920
4993
  tabs = convertContainerToTabs(navigation, context, "Documentation", projectionTrace);
4921
4994
  if (tabs.length === 0 && Array.isArray(config.navigation)) {
4922
4995
  const children = convertNavigationValues(config.navigation, context);
@@ -4996,7 +5069,7 @@ function projectMintlifyNavigation(config, options = {}) {
4996
5069
  ...typeof redirect.permanent === "boolean" ? { permanent: redirect.permanent } : {}
4997
5070
  }];
4998
5071
  }) : [];
4999
- collectGatedReferences(navigation, void 0, options.pathPrefix, gated);
5072
+ collectReferenceAccess(navigation, OPEN_NAVIGATION_ACCESS, options.pathPrefix, appearances);
5000
5073
  return {
5001
5074
  docsConfig: {
5002
5075
  tabs,
@@ -5011,7 +5084,7 @@ function projectMintlifyNavigation(config, options = {}) {
5011
5084
  ...redirects.length > 0 ? { redirects } : {}
5012
5085
  },
5013
5086
  pageReferences: references,
5014
- gatedReferences: gated,
5087
+ referenceAccess: appearances,
5015
5088
  gatedSourceRefs,
5016
5089
  warnings
5017
5090
  };
@@ -8567,7 +8640,7 @@ function flattenTabs(config) {
8567
8640
  });
8568
8641
  }
8569
8642
  function withheldNotSaved(message) {
8570
- return message.replace(/ and saved under migration-quarantine\/ \(local only[^)]*\)/g, " (not saved)").replace(/ and saved under migration-quarantine\/assets\//g, " (not saved)").replace(/, so it was NOT published and was not saved under migration-quarantine\/;/g, ", so it was NOT published and is not saved;").replace(/, so it was not copied to migration-quarantine\/;/g, ", so it is not saved;").replace(/The original is saved at migration-quarantine\/[^;]*;/g, "It is withheld from the site (not saved);").replace(/; review migration-quarantine\/assets\/ and copy any that published pages need\./g, "; they are withheld from the site (not saved).").replace(/Copy any that published pages need from migration-quarantine\/assets\/ into public\/ by hand\./g, "They are withheld from the site (not saved); recover any that published pages need from the referenced repository.").replace(/migration-quarantine\/\S*/g, "withheld from the site (not saved)");
8643
+ return message.replace(/ and saved under migration-quarantine\/ \(local only[^)]*\)/g, " (not saved)").replace(/ and saved under migration-quarantine\/assets\//g, " (not saved)").replace(/; they are saved under migration-quarantine\/assets\/\. Copy them into public\/ manually if they may be public, or/g, "; they are withheld from the site (not saved). Recover any that may be public from the referenced repository, or").replace(/, so it was NOT published and was not saved under migration-quarantine\/;/g, ", so it was NOT published and is not saved;").replace(/, so it was not copied to migration-quarantine\/;/g, ", so it is not saved;").replace(/The original is saved at migration-quarantine\/[^;]*;/g, "It is withheld from the site (not saved);").replace(/; review migration-quarantine\/assets\/ and copy any that published pages need\./g, "; they are withheld from the site (not saved).").replace(/Copy any that published pages need from migration-quarantine\/assets\/ into public\/ by hand\./g, "They are withheld from the site (not saved); recover any that published pages need from the referenced repository.").replace(/migration-quarantine\/\S*/g, "withheld from the site (not saved)");
8571
8644
  }
8572
8645
  function empty(mapping, message) {
8573
8646
  return { ...mapping, pages: [], assets: [], navigation: [], warnings: [{ code: "skipped-file", message: `sourceRef ${mapping.repo} was not imported: ${message}` }] };
@@ -9949,6 +10022,9 @@ function readFrontmatterHead(path) {
9949
10022
  closeSync(fd);
9950
10023
  }
9951
10024
  }
10025
+ function ownRestrictionReason(access) {
10026
+ return access.problem ?? (access.groups?.length ? "frontmatter `groups`" : "frontmatter `public: false`");
10027
+ }
9952
10028
  function canonicalPath(path) {
9953
10029
  try {
9954
10030
  return realpathSync3.native(path);
@@ -9956,11 +10032,15 @@ function canonicalPath(path) {
9956
10032
  return path;
9957
10033
  }
9958
10034
  }
9959
- function pageHeadGateReason(raw) {
10035
+ function pageHeadAccess(raw) {
9960
10036
  const parsed = parseFrontmatter(raw);
9961
- const unreadable = parsed.error && /^(groups|public)\s*:/m.test(raw.slice(0, raw.length - parsed.content.length)) ? "frontmatter could not be parsed and declares `groups` or `public`" : void 0;
10037
+ const unreadable = parsed.error && /^(groups|public)\s*:/m.test(raw.slice(0, raw.length - parsed.content.length));
9962
10038
  const openapi = typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0;
9963
- return { reason: frontmatterGateReason(parsed.data) ?? unreadable, publicTrue: isPublicTrue(parsed.data.public), openapi };
10039
+ const access = unreadable ? { problem: "frontmatter could not be parsed and declares `groups` or `public`" } : frontmatterAccess(parsed.data);
10040
+ return { access, openapi };
10041
+ }
10042
+ function describeRestriction(access) {
10043
+ return access.groups?.length ? `restricted to groups ${access.groups.join(", ")}` : "`public: false`";
9964
10044
  }
9965
10045
  function withoutFrontmatter(value) {
9966
10046
  return value.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
@@ -10818,7 +10898,7 @@ function migrateRepository(options) {
10818
10898
  const referenceOrder = /* @__PURE__ */ new Map();
10819
10899
  let docusaurusSidebars = null;
10820
10900
  let mintlifyConfig = null;
10821
- const mintlifyGatedRefs = /* @__PURE__ */ new Map();
10901
+ const mintlifyNavigationAccess = /* @__PURE__ */ new Map();
10822
10902
  let fernRawConfig = null;
10823
10903
  let fernBasePath = "";
10824
10904
  let fernVersionPath = "";
@@ -10859,9 +10939,11 @@ function migrateRepository(options) {
10859
10939
  docsConfig = projected.docsConfig;
10860
10940
  warnings.push(...projected.warnings);
10861
10941
  for (const repo of projected.gatedSourceRefs) gatedSourceRepos.add(repo.toLowerCase());
10862
- for (const gated of projected.gatedReferences) {
10863
- const gatedKey = normalizedReferenceKey(gated.ref).toLowerCase();
10864
- if (!mintlifyGatedRefs.has(gatedKey)) mintlifyGatedRefs.set(gatedKey, gated.reason);
10942
+ for (const appearance of projected.referenceAccess) {
10943
+ const accessKey = normalizedReferenceKey(appearance.ref).toLowerCase();
10944
+ const known = mintlifyNavigationAccess.get(accessKey);
10945
+ if (known) known.push(appearance.access);
10946
+ else mintlifyNavigationAccess.set(accessKey, [appearance.access]);
10865
10947
  }
10866
10948
  for (const [index, reference] of projected.pageReferences.entries()) {
10867
10949
  const key = normalizedReferenceKey(reference.ref);
@@ -10985,6 +11067,7 @@ function migrateRepository(options) {
10985
11067
  const docusaurusDocCardRoutes = /* @__PURE__ */ new Set();
10986
11068
  const seenPageIds = /* @__PURE__ */ new Set();
10987
11069
  const quarantinedFiles = [];
11070
+ const restrictedPages = /* @__PURE__ */ new Set();
10988
11071
  const withheldAssetPaths = /* @__PURE__ */ new Set();
10989
11072
  const publishedExact = /* @__PURE__ */ new Set();
10990
11073
  const publishedLoose = /* @__PURE__ */ new Set();
@@ -10998,19 +11081,21 @@ function migrateRepository(options) {
10998
11081
  const withheldPaths = /* @__PURE__ */ new Set();
10999
11082
  const isDocFile = (file) => [".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase());
11000
11083
  const isGateCandidate = (file) => isDocFile(file) && !file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
11084
+ const navigationAccessFor = (relativePath) => mergeNavigationAppearances(mintlifyNavigationAccess.get(normalizedReferenceKey(relativePath).toLowerCase()) ?? []);
11001
11085
  const classifyPageGate = (file) => {
11002
- let head = { publicTrue: false };
11003
- let unreadableGate;
11086
+ let head;
11004
11087
  try {
11005
11088
  const size = lstatSync6(file.absolutePath).size;
11006
11089
  const raw = readFrontmatterHead(file.absolutePath) ?? (size > MAX_PAGE_BYTES ? void 0 : readFileSync6(file.absolutePath, "utf8"));
11007
11090
  if (raw === void 0) throw new Error("frontmatter is not terminated within the bounded read");
11008
- head = pageHeadGateReason(raw);
11091
+ head = pageHeadAccess(raw);
11009
11092
  } catch {
11010
- unreadableGate = "frontmatter could not be read";
11093
+ head = { access: { problem: "frontmatter could not be read" } };
11011
11094
  }
11012
- const reason = head.reason ?? unreadableGate ?? mintlifyGatedRefs.get(normalizedReferenceKey(file.relativePath).toLowerCase());
11013
- return { reason, publicTrue: head.publicTrue, openapi: head.openapi };
11095
+ const verdict = resolvePageAccess(head.access, navigationAccessFor(file.relativePath));
11096
+ if (verdict.problem) return { reason: verdict.problem, quarantineReason: verdict.problem, openapi: head.openapi };
11097
+ const access = verdict.access ?? {};
11098
+ return { ...isRestrictedAccess(access) ? { reason: describeRestriction(access) } : {}, access, openapi: head.openapi };
11014
11099
  };
11015
11100
  const keptFiles = new Set(files);
11016
11101
  const droppedGatedFiles = [];
@@ -11026,7 +11111,7 @@ function migrateRepository(options) {
11026
11111
  }
11027
11112
  if (!isGateCandidate(file)) continue;
11028
11113
  const verdict = classifyPageGate(file);
11029
- gateByPath.set(file.absolutePath, { reason: verdict.reason, publicTrue: verdict.publicTrue });
11114
+ gateByPath.set(file.absolutePath, verdict);
11030
11115
  if (verdict.openapi) (verdict.reason ? withheldSpecRefs : publishedSpecRefs).push(verdict.openapi);
11031
11116
  if (verdict.reason) withheldPaths.add(file.absolutePath);
11032
11117
  }
@@ -11074,10 +11159,12 @@ function migrateRepository(options) {
11074
11159
  } catch {
11075
11160
  return { kind: "unreadable", reason: "could not be read" };
11076
11161
  }
11077
- const gated = head === void 0 ? void 0 : pageHeadGateReason(head).reason;
11078
- if (gated) return { kind: inSnippetDirectory ? "snippet-gated" : "gated", reason: gated };
11079
- const navigationGated = inSnippetDirectory ? void 0 : mintlifyGatedRefs.get(normalizedReferenceKey(shown).toLowerCase());
11080
- if (navigationGated) return { kind: "gated", reason: navigationGated };
11162
+ const own2 = head === void 0 ? void 0 : pageHeadAccess(head).access;
11163
+ if (own2 && isRestrictedAccess(own2)) return { kind: inSnippetDirectory ? "snippet-gated" : "gated", reason: ownRestrictionReason(own2) };
11164
+ if (!inSnippetDirectory) {
11165
+ const navigation = navigationAccessFor(shown);
11166
+ if (isNavigationRestricted(navigation)) return { kind: "gated", reason: navigation.problem ?? "its navigation container restricts it" };
11167
+ }
11081
11168
  if (head !== void 0 && parseFrontmatter(head).error) return { kind: "unreadable", reason: "frontmatter could not be parsed" };
11082
11169
  if (lstatSync6(candidate).size > MAX_PAGE_BYTES) return { kind: "oversized", reason: "over 2 MB" };
11083
11170
  return head === void 0 ? { kind: "unreadable", reason: "frontmatter not terminated in the bounded read" } : void 0;
@@ -11192,10 +11279,11 @@ function migrateRepository(options) {
11192
11279
  continue;
11193
11280
  }
11194
11281
  const key = normalizedReferenceKey(file.relativePath);
11282
+ let pageGate;
11195
11283
  if (platform === "mintlify") {
11196
- const gate = gateByPath.get(file.absolutePath);
11197
- if (gate?.publicTrue) sawPublicTrue = true;
11198
- const gateReason = gate?.reason;
11284
+ pageGate = gateByPath.get(file.absolutePath);
11285
+ if (pageGate?.access?.isPublic === true) sawPublicTrue = true;
11286
+ const gateReason = pageGate?.quarantineReason;
11199
11287
  if (gateReason) {
11200
11288
  skipped++;
11201
11289
  if (file.relativePath.split("/").some((segment) => segment === "..")) {
@@ -11222,12 +11310,24 @@ function migrateRepository(options) {
11222
11310
  }
11223
11311
  warnings.push({
11224
11312
  code: "gated-page",
11225
- message: `Access-restricted on the source site (${gateReason}), so it was NOT published. The original is saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}; links from other pages to it will break.`,
11313
+ message: `Access-restricted on the source site, but its access rules could not be migrated safely (${gateReason}), so it was NOT published. Fix the rules in the original, saved at ${QUARANTINE_DIRECTORY}/${file.relativePath}, and move it into src/content/; until then links from other pages to it will break.`,
11226
11314
  source: file.relativePath
11227
11315
  });
11228
11316
  continue;
11229
11317
  }
11318
+ if (pageGate?.reason && mintlifyProjectRoot) {
11319
+ try {
11320
+ rewriteRepositoryAssetLinks(
11321
+ inlineMdxSnippets(readFileSync6(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, [], 0, mintlifyProjectRoot, snippetAliases, inlineGate),
11322
+ file.absolutePath,
11323
+ mintlifyProjectRoot,
11324
+ withholdAsset
11325
+ );
11326
+ } catch {
11327
+ }
11328
+ }
11230
11329
  }
11330
+ const isRestrictedPage = Boolean(pageGate?.reason);
11231
11331
  const referenced = exactReferenceMap.get(exactReferenceKey(file.relativePath)) ?? referenceMap.get(key);
11232
11332
  let locale = referenced?.locale;
11233
11333
  let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
@@ -11257,7 +11357,7 @@ function migrateRepository(options) {
11257
11357
  snippetAliases,
11258
11358
  inlineGate
11259
11359
  );
11260
- const publishedText = trackPublishedRefs ? raw : void 0;
11360
+ const publishedText = trackPublishedRefs && !isRestrictedPage ? raw : void 0;
11261
11361
  const pageAssetReferences = [];
11262
11362
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
11263
11363
  raw = normalizeExplicitHeadingIds(
@@ -11350,7 +11450,8 @@ function migrateRepository(options) {
11350
11450
  if (platform === "fern" && fernHiddenIds.has(navigationId)) page.hidden = true;
11351
11451
  if (platform === "mintlify" && mintlifyProjectRoot) {
11352
11452
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath, onDiskSpelling) => {
11353
- pageAssetReferences.push(publicAssetKey(onDiskSpelling ?? assetPath));
11453
+ if (isRestrictedPage) withholdAsset(assetPath, onDiskSpelling);
11454
+ else pageAssetReferences.push(publicAssetKey(onDiskSpelling ?? assetPath));
11354
11455
  }, (spelled, used) => {
11355
11456
  warnings.push({ code: "unsupported-config", message: `"${spelled}" was not found beside this page; the file "${used}" in the site root was used instead and will be published.`, source: file.relativePath });
11356
11457
  });
@@ -11437,7 +11538,10 @@ function migrateRepository(options) {
11437
11538
  continue;
11438
11539
  }
11439
11540
  seenPageIds.add(page.id);
11541
+ if (pageGate?.access?.groups) page.groups = pageGate.access.groups;
11542
+ if (pageGate?.access?.isPublic !== void 0) page.public = pageGate.access.isPublic;
11440
11543
  pages.push(page);
11544
+ if (isRestrictedPage) restrictedPages.add(page);
11441
11545
  for (const assetPath of pageAssetReferences) addAssetReference(assetPath, file.relativePath);
11442
11546
  if (publishedText !== void 0) addPathReferences(publishedText, posix4.dirname(file.relativePath).replace(/^\.$/, ""), publishedExact, publishedLoose);
11443
11547
  if (hasDocusaurusDocCardList) {
@@ -11533,7 +11637,9 @@ function migrateRepository(options) {
11533
11637
  try {
11534
11638
  rootNavbarButtons = navbarLinkButtons(css);
11535
11639
  rootStyleProjection = projectAuthoredStyles(css, [
11536
- ...pages.map((page) => page.body),
11640
+ // Restricted pages' markup must not keep a rule (and the image its
11641
+ // url() names) public; rules only they use are dropped.
11642
+ ...pages.filter((page) => !restrictedPages.has(page)).map((page) => page.body),
11537
11643
  ...(componentMigrator?.files() ?? []).map((file) => typeof file.content === "string" ? file.content : ""),
11538
11644
  // Site-wide scripts build markup of their own (a CTA card, say) that the stylesheet is meant for.
11539
11645
  ...files.filter((file) => isMintlifyServedScriptOrStyle(file.relativePath) && extname6(file.relativePath).toLowerCase() === ".js" && lstatSync6(file.absolutePath).size <= MAX_PAGE_BYTES).map((file) => readFileSync6(file.absolutePath, "utf8"))
@@ -11866,7 +11972,7 @@ function migrateRepository(options) {
11866
11972
  warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
11867
11973
  };
11868
11974
  const keptSpecRefs = [...publishedSpecRefs, ...navigationOperationRefs(mintlifyConfig)];
11869
- const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages);
11975
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages.filter((page) => !restrictedPages.has(page)));
11870
11976
  for (const remote of remoteApiSpecs) {
11871
11977
  const { withheld, kept } = withheldOperationKeys({ sourcePath: remote.url, filename: remote.url }, withheldSpecRefs, keptSpecRefs);
11872
11978
  if (withheld.length > 0) {
@@ -12360,8 +12466,15 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
12360
12466
  });
12361
12467
  if (platform === "mintlify") {
12362
12468
  docsConfig = { ...docsConfig, markdown: { enabled: true } };
12469
+ if (restrictedPages.size > 0) {
12470
+ const apiNote = [...restrictedPages].some((page) => page.openapi) ? " API operations documented only on restricted pages were withheld from the published OpenAPI spec (specs are not access-controlled), so those pages do not render their endpoint until you restore the operation." : "";
12471
+ warnings.push({
12472
+ code: "gated-page",
12473
+ message: `${restrictedPages.size} page(s) are restricted with \`groups\`/\`public: false\`. Thally hides them from everyone until you configure reader authentication in docs.json \`auth\` (see /guides/authenticated-docs).${apiNote}`
12474
+ });
12475
+ }
12363
12476
  if (quarantinedPageCount > 0 || withheldAssetCount > 0 || unreferencedAssetCount > 0) {
12364
- const assetNote = withheldAssetCount > 0 ? `${withheldAssetCount} file(s) used only by access-restricted pages were kept out of public/ and saved under ${QUARANTINE_DIRECTORY}/assets/. ` : "";
12477
+ const assetNote = withheldAssetCount > 0 ? `${withheldAssetCount} file(s) used only by access-restricted pages were not copied to public/, because public/ is not access-controlled; they are saved under ${QUARANTINE_DIRECTORY}/assets/. Copy them into public/ manually if they may be public, or the restricted pages will show them broken. ` : "";
12365
12478
  const unreferencedReason = [
12366
12479
  ...hasWithheldContent ? ["this site has access-restricted content"] : [],
12367
12480
  ...pagesNotClassified ? ["pages were dropped by the file limit (or the file scan stopped at its cap) and could not all be checked for access restrictions"] : []
@@ -12369,12 +12482,12 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
12369
12482
  const unreferencedNote = unreferencedAssetCount > 0 ? `${unreferencedAssetCount} unreferenced asset(s) were kept out of public/ because ${unreferencedReason}; review ${QUARANTINE_DIRECTORY}/assets/ and copy any that published pages need. ` : "";
12370
12483
  warnings.push({
12371
12484
  code: "gated-page",
12372
- message: (quarantinedPageCount > 0 ? `${quarantinedPageCount} access-restricted page(s) were withheld from the published site and saved under ${QUARANTINE_DIRECTORY}/ (local only: git-ignored, never served or deployed). ` : "") + assetNote + unreferencedNote + "Assets that published pages also use are still copied to public/. Review them before deciding how to publish or protect that content."
12485
+ message: (quarantinedPageCount > 0 ? `${quarantinedPageCount} access-restricted page(s) whose access rules could not be migrated safely were withheld from the published site and saved under ${QUARANTINE_DIRECTORY}/ (local only: git-ignored, never served or deployed). ` : "") + assetNote + unreferencedNote + "Assets that published pages also use are still copied to public/. Review them before deciding how to publish or protect that content."
12373
12486
  });
12374
12487
  }
12375
12488
  warnings.push({
12376
12489
  code: "gated-page",
12377
- message: (sawPublicTrue ? "Some pages set `public: true`, which means the source site used Mintlify authentication and every page WITHOUT it was private. " : "") + "Access control set in the Mintlify dashboard is not visible in the repository. Check the source site's dashboard access settings before publishing: Thally will publish ALL imported pages publicly. Confirm nothing here was meant to stay private before deploying."
12490
+ message: (sawPublicTrue ? "Some pages set `public: true`, which means the source site used Mintlify authentication and every page WITHOUT it was private. " : "") + "Access control set in the Mintlify dashboard is not visible in the repository. Check the source site's dashboard access settings before publishing: until docs.json `auth` is configured, Thally publishes every imported page that has no `groups` or `public: false` to everyone. Confirm nothing here was meant to stay private before deploying."
12378
12491
  });
12379
12492
  }
12380
12493
  if (fernRawConfig?.logo && !logoLight && !logo.light) warnings.push({
@@ -12438,6 +12551,10 @@ function renderPage(bundle, page) {
12438
12551
  page.mode ? `mode: ${yamlString(page.mode)}` : null,
12439
12552
  page.hidden ? "hidden: true" : null,
12440
12553
  page.noindex ? "noindex: true" : null,
12554
+ // Group names must match identity-provider claims exactly, so they are
12555
+ // quoted as-is rather than whitespace-collapsed like display strings.
12556
+ page.groups?.length ? `groups: [${page.groups.map((group) => JSON.stringify(group)).join(", ")}]` : null,
12557
+ page.public !== void 0 ? `public: ${page.public ? "true" : "false"}` : null,
12441
12558
  ...Object.entries(page.meta ?? {}).map(([field, value]) => `${field}: ${yamlString(value)}`),
12442
12559
  page.openapi ? `openapi: ${yamlString(page.openapi)}` : null,
12443
12560
  page.api ? `api: ${yamlString(page.api)}` : null,