Files
zeekayandHanzo Dev f77d5010d8 ee: delete the relocated upstream commercial code and its paywall
Papermark dual-licenses: AGPL-3.0 for the tree, a separate commercial
licence for ee/. We hold the first and not the second, so none of the
second may live here.

ee/ was already absent from main, but it was never deleted. 98f0da7fa
("ci: amd64-only build") renamed 150 files out of ee/ byte-identical
into features/, lib/billing/legacy/ (ee/stripe), lib/billing/limits/
(ee/limits), and flattened app/(ee)/ into app/ — putting commercially
licensed code under our AGPL root LICENSE. A rename is not a deletion.
Every path that commit moved is now gone, edited-since or not.

Deleted, with their routes and UI entry points: features/ai and
app/api/ai, features/workflows and app/api/{workflows,workflow-entry},
features/conversations and the Q&A/FAQ surface, features/security/sso
with app/api/auth/saml + app/api/scim + lib/jackson.ts (we use Hanzo
IAM OIDC natively), features/dataroom-invitations, features/templates,
features/access-notifications, features/conversions, features/
permissions. Five of these were gated by lib/featureFlags, which reads
@vercel/edge-config and returns false for every flag when EDGE_CONFIG
is unset — every non-Vercel deployment we run. Those flags are gone too.

Deleted the commercial gating outright rather than reimplementing it:
lib/billing/legacy (ee/stripe) and lib/billing/limits (ee/limits), the
upgrade modals and banners, UpgradeButton, FeaturePreview, PlanBadge,
the trial and blocking modals, /settings/upgrade, /settings/billing and
the billing checkout/seat/pause/cancel API. usePlan() no longer returns
capability booleans — the plan tag describes provisioning, it is not a
permission — and useLimits() is gone with every quota check it fed.
Subscription pause went the same way. Also dropped the npm dependencies
that existed only for the removed features (@boxyhq/saml-jackson, the
@ai-sdk packages, openai, streamdown, use-stick-to-bottom,
@trigger.dev/python) and the Jackson-specific next.config webpack shims.

Rewritten as original Hanzo code where a capability was load-bearing:
- lib/utils/get-file-size-limits.ts — one UPLOAD_LIMITS set plus
  getFileSizeLimit(contentType), env-overridable. Operational limits,
  not commercial ones, so the tus and browser-upload paths still refuse
  oversized files without consulting a plan matrix.
- components/links/link-sheet/permissions-sheet.tsx — a flat, depth
  annotated item list with view/download switches over the same
  /permission-groups API. Two states, not three. ItemPermission now has
  a single definition in lib/types.ts.
- app/api/links/[id]/upload/route.ts — visitor uploads, built around one
  authorize() helper shared by GET and POST, and a notification debounce
  keyed per (link, viewer) instead of listing and cancelling runs.

There is now exactly one link sheet: the ee DataroomLinkSheet duplicated
the AGPL LinkSheet, which already handled DATAROOM_LINK. Replaced
upstream's Omit<LinkType, "WORKFLOW_LINK"> — Omit on a string union
yields an object type and never narrowed anything — with a real union.

Verified: the S3/tus upload+download harness passes 13/13 against MinIO
after the deletions, and tsc goes from 257 pre-existing errors on
origin/main to 192 with zero files worse than baseline.

Reasoning recorded in LLM.md. LICENSE untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 20:19:21 -07:00

332 lines
9.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type ElementType, useEffect, useRef, useState } from "react";
import { useTeam } from "@/context/team-context";
import { AlertTriangleIcon, CircleCheckIcon, InfoIcon } from "lucide-react";
import { toast } from "sonner";
import { useDebounce } from "use-debounce";
import { useAnalytics } from "@/lib/analytics";
import { validDomainRegex } from "@/lib/domains";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import LoadingSpinner from "@/components/ui/loading-spinner";
const sanitizeDomain = (value: string) =>
value
.trim()
.toLowerCase()
.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "")
.split("/")[0];
type DomainStatus =
| "checking"
| "has site"
| "available"
| "idle"
| "invalid"
| "error";
const STATUS_CONFIG: Record<
DomainStatus,
{
prefix?: string;
useStrong?: boolean;
suffix?: string;
icon?: ElementType;
className?: string;
message?: string;
}
> = {
checking: {
prefix: "Checking availability for",
useStrong: true,
suffix: "...",
icon: LoadingSpinner,
className: "bg-neutral-100 text-neutral-500",
},
"has site": {
suffix:
"is currently pointing to an existing website. Only proceed if you're sure you want to use this domain for Hanzo Dataroom links.",
icon: InfoIcon,
className: "bg-blue-100 text-blue-800",
},
available: {
suffix: "is ready to connect.",
icon: CircleCheckIcon,
className: "bg-emerald-100 text-emerald-600",
},
invalid: {
message: "Enter a valid domain to check availability.",
icon: AlertTriangleIcon,
className: "bg-rose-100 text-rose-600",
},
idle: {
message: "Enter a valid domain to check availability.",
className: "bg-neutral-100 text-neutral-500",
},
error: {
message: "We couldn't check this domain right now. Try again.",
icon: AlertTriangleIcon,
className: "bg-rose-100 text-rose-600",
},
};
export function AddDomainModal({
open,
setOpen,
onAddition,
children,
}: {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onAddition?: (newDomain: string) => void;
children?: React.ReactNode;
}) {
const [domainInput, setDomainInput] = useState<string>("");
const [submitting, setSubmitting] = useState<boolean>(false);
const [domainStatus, setDomainStatus] = useState<DomainStatus>("idle");
const [statusMessageOverride, setStatusMessageOverride] = useState<
string | null
>(null);
const abortRef = useRef<AbortController | null>(null);
const teamInfo = useTeam();
const teamId = teamInfo?.currentTeam?.id;
const analytics = useAnalytics();
useEffect(() => {
if (!open) {
setDomainInput("");
setSubmitting(false);
setDomainStatus("idle");
setStatusMessageOverride(null);
}
}, [open]);
const sanitizedDomain = sanitizeDomain(domainInput);
const [debouncedDomain] = useDebounce(sanitizedDomain, 500);
useEffect(() => {
if (!open) return;
if (!teamId) return;
if (!debouncedDomain) {
setDomainStatus("idle");
setStatusMessageOverride(null);
return;
}
if (debouncedDomain.includes("hanzo")) {
setDomainStatus("invalid");
setStatusMessageOverride("Domain cannot contain 'hanzo'.");
return;
}
if (!validDomainRegex.test(debouncedDomain)) {
setDomainStatus("idle");
setStatusMessageOverride(null);
return;
}
// Abort any in-flight validation request before starting a new one
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setDomainStatus("checking");
setStatusMessageOverride(null);
fetch(
`/api/teams/${teamId}/domains/${encodeURIComponent(
debouncedDomain,
)}/validate`,
{ signal: controller.signal },
)
.then(async (res) => res.json())
.then((data) => {
const nextStatus = data?.status as DomainStatus | undefined;
if (
nextStatus &&
["invalid", "has site", "available"].includes(nextStatus)
) {
setDomainStatus(nextStatus);
} else {
setDomainStatus("error");
}
})
.catch((err) => {
// Ignore aborted requests – they are expected when the user types again
if ((err as DOMException).name === "AbortError") return;
setDomainStatus("error");
});
return () => {
controller.abort();
abortRef.current = null;
};
}, [debouncedDomain, open, teamId]);
const saveDisabled =
!["available", "has site"].includes(domainStatus) || submitting;
const handleSubmit = async (event: any) => {
event.preventDefault();
event.stopPropagation();
const normalizedDomain = sanitizeDomain(domainInput);
if (!normalizedDomain || !validDomainRegex.test(normalizedDomain)) {
return toast.error("Please enter a valid domain (e.g., example.com).");
}
if (normalizedDomain.includes("hanzo")) {
return toast.error("Domain cannot contain 'hanzo'.");
}
if (saveDisabled) {
return toast.error(
statusMessageOverride ?? "Please enter a valid domain before adding.",
);
}
setSubmitting(true);
try {
const response = await fetch(`/api/teams/${teamId}/domains`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
domain: normalizedDomain,
}),
});
if (!response.ok) {
const { message } = await response.json();
toast.error(message);
return;
}
const newDomain = await response.json();
analytics.capture("Domain Added", { slug: normalizedDomain });
toast.success("Domain added successfully! 🎉");
// Update local data with the new link
onAddition && onAddition(newDomain);
setOpen(false);
!onAddition && window.open("/settings/domains", "_blank");
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : "An unknown error occurred";
toast.error(`Failed to add domain: ${message}`);
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader className="text-start">
<DialogTitle>Add Domain</DialogTitle>
<DialogDescription>
Add a custom domain and verify it with DNS.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<Label htmlFor="domain" className="opacity-80">
Your domain
</Label>
{(() => {
const currentStatus = STATUS_CONFIG[domainStatus];
const StatusIcon = currentStatus.icon;
return (
<div
className={cn(
"-m-1 mt-2 rounded-[0.625rem] p-1",
currentStatus.className || "bg-neutral-100 text-neutral-500",
)}
>
<div className="flex rounded-md border border-neutral-300 bg-white">
<Input
id="domain"
placeholder="docs.yourdomain.com"
className="border-0 bg-transparent shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={domainInput}
onBlur={() => {
const normalized = sanitizeDomain(domainInput);
if (normalized && normalized !== domainInput) {
setDomainInput(normalized);
}
}}
onChange={(e) => {
// Cancel any in-flight validation so stale results
// don't overwrite the reset status below
abortRef.current?.abort();
abortRef.current = null;
setDomainStatus("idle");
setStatusMessageOverride(null);
setDomainInput(e.target.value);
}}
/>
</div>
<div className="flex items-center justify-between gap-4 p-2 text-sm">
<p>
{["checking", "has site", "available"].includes(
domainStatus,
) ? (
<>
{currentStatus.prefix || "The domain"}{" "}
{currentStatus.useStrong ? (
<strong className="font-semibold underline underline-offset-2">
{sanitizedDomain || "this domain"}
</strong>
) : (
<span className="font-semibold underline underline-offset-2">
{sanitizedDomain || "this domain"}
</span>
)}{" "}
{currentStatus.suffix}
</>
) : (
statusMessageOverride ||
currentStatus.message ||
"Enter a valid domain to check availability."
)}
</p>
{StatusIcon && <StatusIcon className="h-5 w-5 shrink-0" />}
</div>
</div>
);
})()}
<DialogFooter className="mt-6">
<Button
type="submit"
className="h-9 w-full"
disabled={saveDisabled}
>
{submitting ? "Adding domain..." : "Add domain"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}