{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\n\nimport { joinPaths } from './pathUtils';\nimport { NavigationState, PathState } from './TinkerableContext';\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem, and the join\n * into it — both now defined ONCE in `@immediately-run/platform-constants` (R3-275)\n * and re-exported here so every import site is unchanged.\n *\n * They were declared here AND in `sandbox/src/fsLayout.ts`, with two different\n * implementations of the join (this one over `joinPaths`, the sandbox's hand-rolled).\n * The two agreed; nothing made them agree — and the metadata key space is derived\n * from this constant on both sides, so a drift would have split that key space\n * silently. The package's tests replicate BOTH implementations, quirks included.\n *\n * The sandbox fs is rooted at `/` (so apps can reach dynamic mounts like\n * `/firestore`), with the repo mounted at `APP_ROOT`. URL subpaths are\n * repo-relative, so the file router resolves them under it.\n */\nexport { APP_ROOT, underAppRoot };\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref: string) => {\n  const url = new URL(outerHref);\n  return `${url.protocol}//${url.host}`;\n};\n\nexport const getSearchParams = (search?: string): Record<string, string> =>\n  Object.fromEntries([...new URLSearchParams(search ?? window.location.search).entries()]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n  const i = target.indexOf('#');\n  return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n  const newNavigation = { ...navigation };\n  let [prehash, hash] = target.split('#');\n  if (prehash) {\n    let [path, search] = prehash.split('?');\n    if (path) {\n      newNavigation.sandboxPath = path;\n    }\n    newNavigation.search = search ? search : '';\n  }\n  newNavigation.hash = hash ? hash : '';\n  return newNavigation;\n};\n\nexport const maybeParseUrl = (str: string): URL | null => {\n  try {\n    return new URL(str);\n  } catch (_) {\n    return null;\n  }\n};\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref: string, navigationState: NavigationState) =>\n  constructUrl(outerHref, {\n    ...navigationState,\n    sandboxPath: '',\n    hash: '',\n    search: '',\n  });\n\nexport const constructOuterUrl = (\n  previousOuterHref: string,\n  sandboxTarget: string,\n  navigationState: NavigationState,\n  addFilesPrefix = true,\n): string => {\n  if (isAbsolutePath(sandboxTarget)) {\n    // Split a trailing `#fragment` off before the target is folded into the\n    // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n    // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n    // absolute target with no fragment clears any stale hash from the prior state.\n    const [pathPart, hash] = splitHash(sandboxTarget);\n    return constructUrl(previousOuterHref, {\n      ...navigationState,\n      sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n      hash,\n    });\n  }\n  return new URL(sandboxTarget, constructUrl(previousOuterHref, navigationState)).toString();\n};\n\nexport const isInternalHref = (outerHref: string, target: string, navigationState: NavigationState) => {\n  const parsedUrl = maybeParseUrl(target);\n  if (parsedUrl) {\n    return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n  }\n  // if target is not a valid URL, then assume it's relative.\n  return true;\n};\n\n/**\n * The href to render for a PLATFORM-space path — `/present/github/…`, `/edit/github/…`,\n * `/home`, `/settings/language-model`. These are the HOST's URLs, not this app's: inside the\n * sandboxed frame a root-relative `href` resolves against the SANDBOX origin, which serves no\n * such page, so the path is resolved against the OUTER origin (`outerHref`) instead. An\n * absolute `path` (`https://…`) is returned as-is, which `new URL` already does. With no host\n * (`vite dev`, an empty `outerHref`) or an unresolvable pair, `path` is returned unchanged so\n * the local dev server keeps working. A non-string `path` throws: a caller passing one has a\n * bug worth surfacing, not silently rendering.\n *\n * Render the result through `PlatformLink` (`./platformLink`), which does the part an href\n * cannot: it asks the HOST to navigate. An anchor alone does NOT reach a platform route from\n * inside the app frame — the sandbox withholds top-navigation deliberately, so `target=\"_top\"`\n * is refused by the browser (R3-568). A consumer that renders its own anchor from this href\n * gets copy-link and open-in-new-tab, but its plain clicks will do nothing unless it asks the\n * host too.\n */\nexport const platformHref = (outerHref: string, path: string): string => {\n  if (typeof path !== 'string') {\n    throw new TypeError(`platformHref: path must be a string, got ${typeof path}`);\n  }\n  if (!outerHref) return path;\n  try {\n    return new URL(path, outerHref).toString();\n  } catch {\n    return path;\n  }\n};\n\nexport type PathSegment = {\n  name: string;\n  pattern: string;\n  transform?: (pathSegment: string) => string;\n  // When true, the leading slash that delimits this segment is optional, so the\n  // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n  // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n  // slash, no sub-path) must still parse, otherwise the regex matches nothing\n  // and every segment comes back empty.\n  optionalLeadingSlash?: boolean;\n  // Inverse of `transform`, applied when BUILDING a url (`constructUrl`). Without it a\n  // decoded ref would be written back raw and split into two segments again.\n  encode?: (value: string) => string;\n};\n\n// One URL path segment is \"anything but a slash\". The previous patterns instead\n// ENUMERATED the allowed characters (`[a-zA-Z0-9-_]+`), which silently rejected three\n// real shapes — and rejection here is not an error, it is a whole-match failure that\n// makes `parsePath` return every field EMPTY, so the app's navigation state just goes\n// blank with nothing logged:\n//\n//   /…/repo/v1.2.3/…      a semver TAG              (dot)\n//   /…/foo.js/main/…      a dotted REPOSITORY name  (dot — very common)\n//   /…/repo/claude%2Fx/…  a `/`-containing REF, percent-encoded as one segment\n//\n// The third is the one that motivated this (a ref may contain `/`; the host now encodes\n// it — see site-main `encodeRef`), but the first two were already broken and are the\n// same mistake. `[^/]+` is both the fix and the accurate description of a segment.\n/** Percent-decode one path segment, tolerating a malformed escape: this is untrusted\n *  URL input, and `decodeURIComponent('%zz')` throws. A bad ref must not blank out the\n *  whole navigation state. */\nconst decodeSegment = (value: string): string => {\n  try {\n    return decodeURIComponent(value);\n  } catch {\n    return value;\n  }\n};\n\nconst PATH_SEGMENTS: PathSegment[] = [\n  { name: 'mode', pattern: '\\\\w+' },\n  { name: 'provider', pattern: '[^/]+' },\n  { name: 'namespace', pattern: '[^/]+' },\n  { name: 'repository', pattern: '[^/]+' },\n  // The ref is percent-encoded by the host so a `/` inside it stays ONE segment; decode\n  // on the way in and re-encode on the way out (`encode`), so app code always sees the\n  // real ref (`claude/x`) and never the wire form.\n  { name: 'ref', pattern: '[^/]+', transform: decodeSegment, encode: encodeURIComponent },\n  { name: 'sandboxPath', pattern: '.*', transform: (s) => `/${s}`, optionalLeadingSlash: true },\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n  '^' +\n    PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n      optionalLeadingSlash ? `(?:\\/(?<${name}>${pattern}))?` : `\\/(?<${name}>${pattern})`,\n    ).join('') +\n    '$',\n);\n\nexport const parsePath = (pathname: string): PathState => {\n  const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n  return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n    let value: string | undefined = undefined;\n    if (name in matchResults) {\n      value = matchResults[name];\n    }\n    if (!value) {\n      // fall back to default value if var not present in\n      value = '';\n    }\n    if (typeof value === 'string') {\n      acc[name] = transform ? transform(value) : value;\n    }\n    return acc;\n  }, {}) as PathState;\n};\n\nexport const parseHref = (href: string): NavigationState => {\n  const parsedUrl = new URL(href);\n  const pathnameState = parsePath(parsedUrl.pathname);\n  return {\n    ...pathnameState,\n    search: parsedUrl.search.substring(1),\n    hash: parsedUrl.hash.substring(1),\n  } as NavigationState;\n};\n\nconst stripSlashPrefix = (s: string): string => (s.startsWith('/') ? s.substring(1) : s);\n\nexport const constructUrl = (outerHref: string, navigationState: NavigationState): string => {\n  const path = PATH_SEGMENTS.map(({ name, encode }) => {\n    const value = stripSlashPrefix(navigationState[name] ?? '');\n    // Encode AFTER stripping the delimiter slash: `encode` is per-segment, and the\n    // sandboxPath (which has no `encode`) keeps its own internal slashes.\n    return encode ? encode(value) : value;\n  }).join('/');\n  const host = getOuterHostname(outerHref);\n  let url = `${host}/${path}`;\n  if (navigationState.search) {\n    url += '?' + navigationState.search;\n  }\n  if (navigationState.hash) {\n    url += '#' + navigationState.hash;\n  }\n  return url;\n};\n"],"mappings":"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","names":[]}