stack/packages/shared/src/utils/urls.tsx
BilalG1 c14a9dd3d0
feat(hexclave): PR 5 — internal symbol/path/package renames + brand strings (#1547)
## Stack Auth → Hexclave rename — PR 5 (internal symbols, paths,
packages, brand strings)

PR 5 finishes the **internal / non-wire** half of the Stack→Hexclave
rename. It only touches things where nothing outside the repo depends on
the exact name: internal symbols, file/dir names, the
`@stackframe/template` package, and residual brand strings. Plan +
progress are in `HEXCLAVE-RENAME-PR5-PLAN.md`.

Every step was verified green (`pnpm typecheck` + `pnpm lint`, 28/28)
and committed as its own checkpoint, then a fan-out of review agents
audited all commits and the findings were fixed.

### What changed
- **Internal symbols** (`@hexclave/shared`, `packages/template`, apps):
`stack*`/`Stack*` → `hexclave*`/`Hexclave*` — incl.
`stackGlobalsSymbol`, the `_Stack*AppImpl` classes,
`stackAppInternalsSymbol`, `StackContext`, `getStackStripe`, etc. The
`stack*App` local-variable convention
(`stackServerApp`/`stackClientApp`/…) was renamed across 175
source/example/doc files.
- **File renames**: `hexclave-handler/provider/context.tsx`,
`backend/hexclave.tsx`, `internal-tool/hexclave.ts`,
`hexclave-app-internals.ts`.
- **Directory renames**: `lib/hexclave-app`, `hexclave-companion`,
`[...hexclave]` route segment, `skills/hexclave`,
`dashboard/src/hexclave`, and the package dirs
**`packages/{next,shared,ui,sc,cli}`** (dropping the `stack-` prefix to
match the `@hexclave/*` npm names).
- **Packages**: `@stackframe/template` → `@hexclave/template`; **deleted
`packages/init-stack`** (onboarding lives in `@hexclave/cli init`; the
published npm package is untouched).
- **Brand strings**: reworded `Stack Auth`/`Stack dashboard` prose in
code + docs-mintlify, renamed `hexclave-app.mdx`/`use-hexclave-app.mdx`
with redirects, regenerated OpenAPI, updated coupled e2e assertions;
`doctor`/`init` now prefer `hexclave.config.ts`.

### Intentionally kept (verified, not oversights)
Wire/compat identifiers (`x-stack-*` headers, `stack-*` cookies,
`STACK_*` env names, `*.stack-auth.com`, `stackauth_`, `ask_stack_auth`,
query params), public `Stack*` SDK aliases, crypto/JWT/vault
domain-separation tags, `*-brand-sentinel`s, the
`Symbol.for("StackAuth--…")` string, `_stack_sync_metadata`, Postgres
`stackframe` / docker image names, the `stack-auth-logo*.svg` (used by
the rebrand modal), and `migration.mdx` / "formerly known as Stack Auth"
notes. False positives (Phosphor `StackIcon`/`StackSimple`, `TanStack`,
`OrbStack`, `stackable`/`Stacked` charts) left alone.

### Review pass
Six review agents audited all commits. Found + fixed one real bug — a
build script (`bundle-type-definitions.ts`) hardcoded the old
`lib/stack-app` glob path (not an import, so typecheck/lint were blind),
silently emptying the dashboard AI type bundle — plus stale comments, a
dead CI env var, and stale `.gitignore`/`.dockerignore` entries.
Cross-cutting audit confirmed **zero wire-compat identifiers were
accidentally renamed**.

### ⚠️ Verification note
`typecheck` + `lint` are fully green locally. The **e2e suite was not
run** (needs a live backend+DB), so the brand-string assertion +
OpenAPI-regen changes are verified by grep/codegen only — please let CI
exercise e2e to confirm.

### Base-branch note
This branch was forked from the local-only `cl/friendly-lewin-72293f`
(not on origin, no separate PR), so this PR against `dev` also carries
that branch's ~11 preceding Hexclave-rename commits (config-file rename,
env-var dual-read, AI setup-prompt rebrand). If those should land
separately, re-parent before merge.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Finishes the internal Stack Auth → Hexclave rename and cleans up
remaining stragglers, including dev-tool and prompt copy. All changes
are internal-only; public/wire APIs remain unchanged. Re-merged `dev`
and resolved the payments create-purchase-url conflict.

- **Refactors**
- Internal symbols: stack*/Stack* → hexclave*/Hexclave* (e.g.,
`getHexclaveServerApp` via `@/hexclave`, `getHexclaveStripe`,
`hexclaveAppInternalsSymbol`, `hexclaveSchemaInfo`, Prisma
`__hexclave_*`, `data-hexclave-handler-page`, Stripe mock
`hexclavePortPrefix`).
- Files/dirs: moved to `lib/hexclave-app`; handler route
`[...hexclave]`; backend entry `src/hexclave.tsx`; dashboard internals
`hexclave-app-internals`; companion `hexclave-companion`; dropped
`stack-` prefix across package dirs
(`packages/{shared,ui,sc,cli,next}`); workflows/emulator paths now
`packages/cli`; Quetzal codegen env at `packages/next/.env.local`.
- Packages/docs: `@stackframe/template` → `@hexclave/template`; removed
`packages/init-stack`; regenerated OpenAPI and updated docs
slugs/redirects for hexclave-app/use-hexclave-app.
- Brand strings/prompts: reworded remaining “Stack” dashboard strings to
Hexclave; updated dev-tool copy and prompts; `doctor/init` now prefer
`hexclave.config.ts`. Kept all wire-compat identifiers and public
aliases (`x-stack-*`, `stack-*` cookies, `STACK_*` env,
`*.stack-auth.com`, `Stack*` SDK names).
- Rebased/merged onto latest `dev`: retained `@hexclave/template`, kept
`src` in published files, refreshed setup-prompt imports and docs JSON,
adopted 1.0.5 version bumps, and re-merged `dev` again (resolved
`create-purchase-url` with `getHexclaveStripe`).

- **Bug Fixes**
- Restored dashboard AI type bundle by pointing the glob to
`packages/template/src/lib/hexclave-app`.
- Addressed rename leftovers: updated lingering `@/stack` imports and
CSS selector, fixed schema/meta and port-prefix expansions, and aligned
emulator commands to `packages/cli`.
- CI/build: removed a dead env var and stale ignore entries; fixed
Docker by renaming `STACK_SKIP_TEMPLATE_GENERATION` →
`HEXCLAVE_SKIP_TEMPLATE_GENERATION`.

<sup>Written for commit 3c1af3bff3.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1547?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->
2026-06-03 18:57:09 -07:00

412 lines
17 KiB
TypeScript

import { generateSecureRandomString } from "./crypto";
import { templateIdentity } from "./strings";
export function createUrlIfValid(...args: ConstructorParameters<typeof URL>) {
try {
return new URL(...args);
} catch (e) {
return null;
}
}
import.meta.vitest?.test("createUrlIfValid", ({ expect }) => {
// Test with valid URLs
expect(createUrlIfValid("https://example.com")).toBeInstanceOf(URL);
expect(createUrlIfValid("https://example.com/path?query=value#hash")).toBeInstanceOf(URL);
expect(createUrlIfValid("/path", "https://example.com")).toBeInstanceOf(URL);
// Test with invalid URLs
expect(createUrlIfValid("")).toBeNull();
expect(createUrlIfValid("not a url")).toBeNull();
expect(createUrlIfValid("http://")).toBeNull();
});
export function isValidUrl(url: string) {
return !!createUrlIfValid(url);
}
import.meta.vitest?.test("isValidUrl", ({ expect }) => {
// Test with valid URLs
expect(isValidUrl("https://example.com")).toBe(true);
expect(isValidUrl("http://localhost:3000")).toBe(true);
expect(isValidUrl("ftp://example.com")).toBe(true);
// Test with invalid URLs
expect(isValidUrl("")).toBe(false);
expect(isValidUrl("not a url")).toBe(false);
expect(isValidUrl("http://")).toBe(false);
});
export function isValidHostname(hostname: string) {
// Basic validation
if (!hostname || hostname.startsWith('.') || hostname.endsWith('.') || hostname.includes('..')) {
return false;
}
const url = createUrlIfValid(`https://${hostname}`);
if (!url) return false;
return url.hostname === hostname;
}
import.meta.vitest?.test("isValidHostname", ({ expect }) => {
// Test with valid hostnames
expect(isValidHostname("example.com")).toBe(true);
expect(isValidHostname("localhost")).toBe(true);
expect(isValidHostname("sub.domain.example.com")).toBe(true);
expect(isValidHostname("127.0.0.1")).toBe(true);
// Test with invalid hostnames
expect(isValidHostname("")).toBe(false);
expect(isValidHostname("example.com/path")).toBe(false);
expect(isValidHostname("https://example.com")).toBe(false);
expect(isValidHostname("example com")).toBe(false);
});
export function isValidHostnameWithWildcards(hostname: string) {
// Empty hostnames are invalid
if (!hostname) return false;
// Check if it contains wildcards
const hasWildcard = hostname.includes('*');
if (!hasWildcard) {
// If no wildcards, validate as a normal hostname
return isValidHostname(hostname);
}
// Basic validation checks that apply even with wildcards
// - Hostname cannot start or end with a dot
if (hostname.startsWith('.') || hostname.endsWith('.')) {
return false;
}
// - No consecutive dots
if (hostname.includes('..')) {
return false;
}
// For wildcard validation, check that non-wildcard parts contain valid characters
// Replace wildcards with a valid placeholder to check the rest
const testHostname = hostname.replace(/\*+/g, 'wildcard');
// Check if the resulting string would be a valid hostname
if (!/^[a-zA-Z0-9.-]+$/.test(testHostname)) {
return false;
}
// Additional check: ensure the pattern makes sense
// Check each segment between wildcards
const segments = hostname.split(/\*+/);
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (segment === '') continue; // Empty segments are OK (consecutive wildcards)
// First segment can't start with dot
if (i === 0 && segment.startsWith('.')) {
return false;
}
// Last segment can't end with dot
if (i === segments.length - 1 && segment.endsWith('.')) {
return false;
}
// No segment should have consecutive dots
if (segment.includes('..')) {
return false;
}
}
return true;
}
import.meta.vitest?.test("isValidHostnameWithWildcards", ({ expect }) => {
// Test with valid regular hostnames
expect(isValidHostnameWithWildcards("example.com")).toBe(true);
expect(isValidHostnameWithWildcards("localhost")).toBe(true);
expect(isValidHostnameWithWildcards("sub.domain.example.com")).toBe(true);
// Test with valid wildcard hostnames
expect(isValidHostnameWithWildcards("*.example.com")).toBe(true);
expect(isValidHostnameWithWildcards("a-*.example.com")).toBe(true);
expect(isValidHostnameWithWildcards("*.*.org")).toBe(true);
expect(isValidHostnameWithWildcards("**.example.com")).toBe(true);
expect(isValidHostnameWithWildcards("sub.**.com")).toBe(true);
expect(isValidHostnameWithWildcards("*-api.*.com")).toBe(true);
// Test with invalid hostnames
expect(isValidHostnameWithWildcards("")).toBe(false);
expect(isValidHostnameWithWildcards("example.com/path")).toBe(false);
expect(isValidHostnameWithWildcards("https://example.com")).toBe(false);
expect(isValidHostnameWithWildcards("example com")).toBe(false);
expect(isValidHostnameWithWildcards(".example.com")).toBe(false);
expect(isValidHostnameWithWildcards("example.com.")).toBe(false);
expect(isValidHostnameWithWildcards("example..com")).toBe(false);
expect(isValidHostnameWithWildcards("*.example..com")).toBe(false);
});
export function matchHostnamePattern(pattern: string, hostname: string): boolean {
// If no wildcards, it's an exact match
if (!pattern.includes('*')) {
return pattern === hostname;
}
// Convert the pattern to a regex
// First, escape all regex special characters except *
let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
// Use a placeholder for ** to handle it separately from single *
const doubleWildcardPlaceholder = '\x00DOUBLE_WILDCARD\x00';
regexPattern = regexPattern.replace(/\*\*/g, doubleWildcardPlaceholder);
// Replace single * with a pattern that matches anything except dots
regexPattern = regexPattern.replace(/\*/g, '[^.]*');
// Replace the double wildcard placeholder with a pattern that matches anything including dots
regexPattern = regexPattern.replace(new RegExp(doubleWildcardPlaceholder, 'g'), '.*');
// Anchor the pattern to match the entire hostname
regexPattern = '^' + regexPattern + '$';
try {
const regex = new RegExp(regexPattern);
return regex.test(hostname);
} catch {
return false;
}
}
import.meta.vitest?.test("matchHostnamePattern", ({ expect }) => {
// Test exact matches
expect(matchHostnamePattern("example.com", "example.com")).toBe(true);
expect(matchHostnamePattern("example.com", "other.com")).toBe(false);
// Test single wildcard matches
expect(matchHostnamePattern("*.example.com", "api.example.com")).toBe(true);
expect(matchHostnamePattern("*.example.com", "www.example.com")).toBe(true);
expect(matchHostnamePattern("*.example.com", "example.com")).toBe(false);
expect(matchHostnamePattern("*.example.com", "api.v2.example.com")).toBe(false);
// Test double wildcard matches
expect(matchHostnamePattern("**.example.com", "api.example.com")).toBe(true);
expect(matchHostnamePattern("**.example.com", "api.v2.example.com")).toBe(true);
expect(matchHostnamePattern("**.example.com", "a.b.c.example.com")).toBe(true);
expect(matchHostnamePattern("**.example.com", "example.com")).toBe(false);
// Test complex patterns
expect(matchHostnamePattern("api-*.example.com", "api-v1.example.com")).toBe(true);
expect(matchHostnamePattern("api-*.example.com", "api-v2.example.com")).toBe(true);
expect(matchHostnamePattern("api-*.example.com", "api.example.com")).toBe(false);
expect(matchHostnamePattern("*.*.org", "mail.example.org")).toBe(true);
expect(matchHostnamePattern("*.*.org", "example.org")).toBe(false);
});
export function getHardcodedFallbackUrls(primaryBaseUrl: string): string[] {
// `defaultBaseUrl` in `@stackframe/*` SDK builds is `https://api.stack-auth.com`
// (see `packages/template/src/lib/hexclave-app/apps/implementations/common.ts`).
// The `@hexclave/*` mirror packages will get this rewritten to the hexclave
// hostname by `scripts/rewrite-packages-to-hexclave.ts` in a follow-up PR; both
// branches below stay listed so either build resolves its own fallback set.
if (primaryBaseUrl === "https://api.stack-auth.com") {
return ["https://api1.stack-auth.com", "https://api2.stack-auth.com"];
}
if (primaryBaseUrl === "https://api.dev.stack-auth.com") {
return ["https://api1.dev.stack-auth.com", "https://api2.dev.stack-auth.com"];
}
if (primaryBaseUrl === "https://api.hexclave.com") {
return ["https://api1.hexclave.com", "https://api2.hexclave.com"];
}
if (primaryBaseUrl === "https://api.dev.hexclave.com") {
return ["https://api1.dev.hexclave.com", "https://api2.dev.hexclave.com"];
}
const localhostMatch = primaryBaseUrl.match(/^http:\/\/localhost:(\d+)02$/);
if (localhostMatch) {
return [`http://localhost:${localhostMatch[1]}10`];
}
return [];
}
export function getDefaultApiUrls(primaryBaseUrl: string): string[] {
return [primaryBaseUrl, ...getHardcodedFallbackUrls(primaryBaseUrl)];
}
export function isLocalhost(urlOrString: string | URL) {
const url = createUrlIfValid(urlOrString);
if (!url) return false;
if (url.hostname === "localhost" || url.hostname.endsWith(".localhost")) return true;
if (url.hostname.match(/^127\.\d+\.\d+\.\d+$/)) return true;
if (url.hostname === "[::1]" || url.hostname === "::1") return true;
return false;
}
import.meta.vitest?.test("isLocalhost", ({ expect }) => {
// Test with localhost URLs
expect(isLocalhost("http://localhost")).toBe(true);
expect(isLocalhost("https://localhost:8080")).toBe(true);
expect(isLocalhost("http://sub.localhost")).toBe(true);
expect(isLocalhost("http://127.0.0.1")).toBe(true);
expect(isLocalhost("http://127.1.2.3")).toBe(true);
expect(isLocalhost("http://[::1]")).toBe(true);
// Test with non-localhost URLs
expect(isLocalhost("https://example.com")).toBe(false);
expect(isLocalhost("http://192.168.1.1")).toBe(false);
expect(isLocalhost("http://10.0.0.1")).toBe(false);
// Test with URL objects
expect(isLocalhost(new URL("http://localhost"))).toBe(true);
expect(isLocalhost(new URL("https://example.com"))).toBe(false);
// Test with invalid URLs
expect(isLocalhost("not a url")).toBe(false);
expect(isLocalhost("")).toBe(false);
});
export function isRelative(url: string) {
const randomDomain = `${generateSecureRandomString()}.stack-auth.example.com`;
const u = createUrlIfValid(url, `https://${randomDomain}`);
if (!u) return false;
if (u.host !== randomDomain) return false;
if (u.protocol !== "https:") return false;
return true;
}
import.meta.vitest?.test("isRelative", ({ expect }) => {
// We can't easily mock generateSecureRandomString in this context
// but we can still test the function's behavior
// Test with relative URLs
expect(isRelative("/")).toBe(true);
expect(isRelative("/path")).toBe(true);
expect(isRelative("/path?query=value#hash")).toBe(true);
// Test with absolute URLs
expect(isRelative("https://example.com")).toBe(false);
expect(isRelative("http://example.com")).toBe(false);
expect(isRelative("//example.com")).toBe(false);
// Note: The implementation treats empty strings and invalid URLs as relative
// This is because they can be resolved against a base URL
expect(isRelative("")).toBe(true);
expect(isRelative("not a url")).toBe(true);
});
export function getRelativePart(url: URL) {
return url.pathname + url.search + url.hash;
}
import.meta.vitest?.test("getRelativePart", ({ expect }) => {
// Test with various URLs
expect(getRelativePart(new URL("https://example.com"))).toBe("/");
expect(getRelativePart(new URL("https://example.com/path"))).toBe("/path");
expect(getRelativePart(new URL("https://example.com/path?query=value"))).toBe("/path?query=value");
expect(getRelativePart(new URL("https://example.com/path#hash"))).toBe("/path#hash");
expect(getRelativePart(new URL("https://example.com/path?query=value#hash"))).toBe("/path?query=value#hash");
// Test with different domains but same paths
const url1 = new URL("https://example.com/path?query=value#hash");
const url2 = new URL("https://different.com/path?query=value#hash");
expect(getRelativePart(url1)).toBe(getRelativePart(url2));
});
/**
* A template literal tag that returns a URL.
*
* Any values passed are encoded.
*/
export function url(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): URL {
return new URL(urlString(strings, ...values));
}
import.meta.vitest?.test("url", ({ expect }) => {
// Test with no interpolation
expect(url`https://example.com`).toBeInstanceOf(URL);
expect(url`https://example.com`.href).toBe("https://example.com/");
// Test with string interpolation
expect(url`https://example.com/${"path"}`).toBeInstanceOf(URL);
expect(url`https://example.com/${"path"}`.pathname).toBe("/path");
// Test with number interpolation
expect(url`https://example.com/${42}`).toBeInstanceOf(URL);
expect(url`https://example.com/${42}`.pathname).toBe("/42");
// Test with boolean interpolation
expect(url`https://example.com/${true}`).toBeInstanceOf(URL);
expect(url`https://example.com/${true}`.pathname).toBe("/true");
// Test with special characters in interpolation
expect(url`https://example.com/${"path with spaces"}`).toBeInstanceOf(URL);
expect(url`https://example.com/${"path with spaces"}`.pathname).toBe("/path%20with%20spaces");
// Test with multiple interpolations
expect(url`https://example.com/${"path"}?query=${"value"}`).toBeInstanceOf(URL);
expect(url`https://example.com/${"path"}?query=${"value"}`.pathname).toBe("/path");
expect(url`https://example.com/${"path"}?query=${"value"}`.search).toBe("?query=value");
});
/**
* A template literal tag that returns a URL string.
*
* Any values passed are encoded.
*/
export function urlString(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): string {
return templateIdentity(strings, ...values.map(encodeURIComponent));
}
import.meta.vitest?.test("urlString", ({ expect }) => {
// Test with no interpolation
expect(urlString`https://example.com`).toBe("https://example.com");
// Test with string interpolation
expect(urlString`https://example.com/${"path"}`).toBe("https://example.com/path");
// Test with number interpolation
expect(urlString`https://example.com/${42}`).toBe("https://example.com/42");
// Test with boolean interpolation
expect(urlString`https://example.com/${true}`).toBe("https://example.com/true");
// Test with special characters in interpolation
expect(urlString`https://example.com/${"path with spaces"}`).toBe("https://example.com/path%20with%20spaces");
expect(urlString`https://example.com/${"?&="}`).toBe("https://example.com/%3F%26%3D");
// Test with multiple interpolations
expect(urlString`https://example.com/${"path"}?query=${"value"}`).toBe("https://example.com/path?query=value");
expect(urlString`https://example.com/${"path"}?query=${"value with spaces"}`).toBe("https://example.com/path?query=value%20with%20spaces");
});
export function isChildUrl(parentUrl: URL, maybeChildUrl: URL) {
return parentUrl.origin === maybeChildUrl.origin
&& isChildPath(parentUrl.pathname, maybeChildUrl.pathname)
&& [...parentUrl.searchParams].every(([key, value]) => maybeChildUrl.searchParams.get(key) === value)
&& (!parentUrl.hash || parentUrl.hash === maybeChildUrl.hash);
}
import.meta.vitest?.test("isChildUrl", ({ expect }) => {
// true
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/"))).toBe(true);
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/path"))).toBe(true);
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/path?query=value"))).toBe(true);
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://abc.com/path?query=value#hash"))).toBe(true);
// false
expect(isChildUrl(new URL("https://abc.com"), new URL("https://example.com"))).toBe(false);
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://example.com/path"))).toBe(false);
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://example.com/path?query=value"))).toBe(false);
expect(isChildUrl(new URL("https://abc.com/"), new URL("https://example.com/path?query=value#hash"))).toBe(false);
expect(isChildUrl(new URL("https://example.com"), new URL("https://abc.com/path?query=value#hash"))).toBe(false);
expect(isChildUrl(new URL("https://example.com?query=value123"), new URL("https://example.com/path?query=value#hash"))).toBe(false);
});
export function isChildPath(parentPath: string, maybeChildPath: string) {
parentPath = parentPath.endsWith("/") ? parentPath : parentPath + "/";
maybeChildPath = maybeChildPath.endsWith("/") ? maybeChildPath : maybeChildPath + "/";
return maybeChildPath.startsWith(parentPath);
}
import.meta.vitest?.test("isSubPath", ({ expect }) => {
expect(isChildPath("/", "/")).toBe(true);
expect(isChildPath("/", "/path")).toBe(true);
expect(isChildPath("/path", "/")).toBe(false);
expect(isChildPath("/path", "/path")).toBe(true);
expect(isChildPath("/path/", "/path")).toBe(true);
expect(isChildPath("/path", "/path/")).toBe(true);
expect(isChildPath("/path/", "/path/")).toBe(true);
expect(isChildPath("/path", "/path/abc")).toBe(true);
expect(isChildPath("/path/", "/path/abc")).toBe(true);
expect(isChildPath("/path", "/path-abc")).toBe(false);
expect(isChildPath("/path", "/path-abc/")).toBe(false);
expect(isChildPath("/path/", "/path-abc")).toBe(false);
expect(isChildPath("/path/", "/path-abc/")).toBe(false);
});