Drop the dual-licensed `ee/` tree entirely. All features previously
locked behind the Hanzo Dataroom Commercial License are now part of the
AGPL-3.0 core and freely available — custom domains, branded viewing,
advanced analytics, watermarks, access controls, Q&A, AI vector stores,
workflows, SAML SSO, dataroom invitations, conversations, templates.
Layout changes:
* `ee/features/{ai,conversations,workflows,permissions,storage,templates,
dataroom-invitations,access-notifications,security,conversions}/` →
`features/<name>/` (AGPL).
* `ee/limits/` → `lib/billing/limits/` with every plan resolving to an
unlimited profile (no users/links/documents/domains/datarooms cap,
conversations and watermarks always on).
* `ee/stripe/` → `lib/billing/legacy/` (already a compat shim over
`@hanzoai/commerce`; renewal-reminder webhook is now a no-op).
* `ee/features/billing/cancellation/` deleted. Pause/unpause/cancel/
retention routes now compile against `lib/billing/cancellation.ts`
stubs that return 410 Gone; `isTeamPaused`/`isTeamPausedById` in
`lib/billing/paused.ts` always report active. `CancellationModal` is
a no-op shim in `components/billing/`.
* `app/(ee)/api/*` route group folded into `app/api/*` (SAML auth,
SCIM, AI chat, workflows, FAQ, link-upload).
Build: `npm run build` green.
Per CTO directive: zero Stripe across all our repos. Migration mirrors
hanzoai/gui@8d05cf6fb3.
- lib/commerce.ts: new hand-rolled Hanzo Commerce REST client + minimal
CommerceTypes namespace covering only what the dataroom touches
(Subscription, Invoice, Customer, CheckoutSession, PortalSession,
Coupon, Event, Checkout.Session). HMAC-SHA256 webhook signature
verification matches the gui pattern.
- ee/stripe/index.ts: now a thin shim — `stripeInstance(...)` returns
the Commerce client; `cancelSubscription(...)` delegates to it.
- ee/stripe/client.ts: Stripe.js loadStripe + redirectToCheckout
replaced with a redirect helper that bounces the browser to
pay.hanzo.ai (@hanzoai/pay). No more card UI in the dataroom.
- ee/stripe/utils.ts: drop `import Stripe from "stripe"`; use
CommerceTypes.Subscription. Stamped TODO(stripe-rip) on the PLANS
const — the Stripe price IDs embedded there still need re-keying
to Commerce plan IDs (5 plans × monthly+yearly × test+prod × new+old
account; coordinate with Commerce admin).
- ee/stripe/webhooks/{checkout-session-completed,customer-subscription-
{updated,deleted},invoice-upcoming}.ts: drop stripe SDK imports;
rebind types to CommerceTypes.
- ee/features/security/lib/fraud-prevention.ts: addEmailToStripeRadar
becomes a no-op with TODO(stripe-rip). Commerce does fraud at the
gateway and does not yet expose a Radar-equivalent endpoint; the
Edge Config blocklist remains the effective block surface.
- ee/features/billing/cancellation/api/*.ts: error message
"No Stripe customer ID" -> "No billing customer ID"; the local
`stripe` variable is now a Commerce instance via stripeInstance().
- pages/api/stripe/webhook.ts + webhook-old.ts: DELETED. Commerce
owns webhook termination; the new dispatcher is at
pages/api/commerce/webhook.ts and reuses the existing per-event
business-logic handlers in ee/stripe/webhooks/.
- pages/api/commerce/webhook.ts: NEW. Accepts hanzo-commerce-signature
(falls back to stripe-signature header so a side-by-side rollout
works), verifies HMAC with HANZO_COMMERCE_WEBHOOK_SECRET, dispatches
to the same handlers as before.
- package.json: removed `stripe` (^16.12.0), `@stripe/stripe-js`
(^4.10.0), and the `stripe:webhook` pkgx script. No new deps — the
Commerce REST client is hand-rolled per spec.
TODO(stripe-rip) markers flag the remaining deep work:
- PLANS const price IDs need re-keying to Commerce plan IDs
- ee/stripe/ directory should be renamed to ee/commerce/
- Local `stripeInstance` identifier should be renamed `commerce`
- Subscription.pause_collection / proration_behavior contract needs
to land in the Commerce OpenAPI spec
- Commerce fraud-blocklist API needs to ship to replace the Radar
call we just stubbed out
`tsc --noEmit` shows zero new errors in ee/, lib/commerce.ts,
pages/api/commerce/, components/billing/. Pre-existing TS errors in
redis-job-store, lib/auth, app routes are unchanged.
- Extract organization from IAM profile (owner/organization/org fields)
- Pass organization through JWT → session → CustomUser
- Add HANZO_IAM_* variables to .env.example
- Deprecate Google OAuth in favor of Hanzo IAM
- Dynamic import DomainMiddleware to avoid pulling ioredis into Edge bundle
- Add hanzo.ai to APP_DOMAINS exclusion list in isCustomDomain
- DomainMiddleware now uses fetch to internal API for Redis lookups
- Create /api/internal/domain-redirect endpoint for Edge-safe KV access
When a link is duplicated, the custom form fields and visitor group
associations were not being copied to the new link. This adds:
- Include customFields and visitorGroups in the Prisma query for the
source link
- Create new CustomField records for the duplicated link via createMany
- Create new LinkVisitorGroup junction records for the duplicated link
- Return customFields and visitorGroups in the created link response
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
The account/general page had a useEffect that automatically opened
an UpgradePlanModal every time a non-annual-plan user navigated to
User Settings. This was disruptive as users would see a billing popup
about upgrading to data rooms on every visit.
Removed the UpgradePlanModal component, the useEffect trigger, the
plan detection logic, and all related imports from the page.
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
The tooltip was using router.query.id (dataroom ID) instead of the
actual document ID, and versionNumber was NaN. Pass documentId through
BarChartComponent data so the tooltip resolves the correct thumbnail.
Add rate limiting (150 req/min per user) to the get-thumbnail endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use RFC 5987 filename* parameter to include the UTF-8 encoded original
filename alongside the ASCII-safe slug, so downloads show the proper
CJK name in modern browsers. Also adds missing Content-Disposition to
put-file-server and stream-file-server uploads.
Co-authored-by: Cursor <cursoragent@cursor.com>
slugify strips all CJK characters, producing empty strings for
filenames and folder names that contain only CJK text. Replace all
usages with safeSlugify which falls back to a 12-char lowercase
alphanumeric nanoid when slugify returns an empty result.
Co-authored-by: Cursor <cursoragent@cursor.com>
When applyAccentColorToDataroomView is enabled but there are not enough
folders/documents to fill the viewport, a white background was visible
underneath the content. Adding min-h-screen ensures the accent background
color extends to fill at least the full viewport height.
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
- Create new Tinybird pipe get_dataroom_view_document_stats for bulk document view stats
- Add API endpoint for dataroom view document stats with per-page chart support
- Create SWR hooks with deferred loading (enabled flag)
- Create reusable DocumentViewDuration, DocumentViewCompletion, DocumentPageChart components
- Create DataroomViewStats component combining history with stats
- Update dataroom-visitors-table.tsx and dataroom-viewers.tsx to track expansion state
- Stats only load when visitor row is expanded (deferred loading)
- Per-page charts only load when individual document line is expanded
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
- Change redirect status from 301 to 302 (non-permanent, user-configurable)
- Remove backfill UPDATE entries from migration SQL
- Remove unnecessary sync-redirects cron endpoint
- Add URL validation: SSRF protection, edge config keyword blocklist,
protocol check, URL sanitization via shared validateRedirectUrl helper
- Add plan gating: require Business plan or higher for redirect URLs
(enforced on both API endpoints and UI)
- Clean up Redis redirect URLs on team deletion
- Clear redirect URLs (Postgres + Redis) on subscription cancellation
and on plan downgrade below Business
- Split redis.ts to avoid Prisma import in edge middleware context
- Gate DomainCard redirect UI based on plan (show upgrade prompt)
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
- Add redirectUrl field to Domain model in Prisma schema
- Create migration for the new field
- Add Redis helper for domain redirect URL caching
- Update middleware to check Redis for redirect URL instead of hardcoded values
- Update domain API: GET includes redirectUrl, POST accepts redirectUrl, PUT updates redirectUrl
- Sync Redis on domain create, update, and delete
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
* feat: add option to allow text selection on Notion pages
Add enableTextSelection field to Link model that allows document owners
to toggle text selection/copying for visitors on Notion pages.
Changes:
- Add enableTextSelection Boolean field to Link Prisma schema
- Add database migration for the new field
- Create TextSelectionSection toggle component for link settings
- Wire the setting through link sheet, link options, API routes
- Pass textSelectionEnabled prop to NotionPage viewer component
- Add .notion-text-selection-enabled CSS class that overrides
user-select: none when text selection is allowed
- Update link-active-controls to show when text selection is active
- Update webhooks and link data queries to include new field
By default, text selection remains disabled (preserving existing
behavior). When enabled, visitors can select and copy text content
on Notion document pages.
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
* refactor: use team feature flag for text selection instead of per-link toggle
Replace the per-link enableTextSelection database field with a team-level
feature flag (textSelection) using the existing Vercel Edge Config system.
This means text selection on Notion pages is controlled at the team level
via the betaFeatures edge config, so there's no per-link toggle to manage.
Changes:
- Remove enableTextSelection from Link Prisma schema and migration
- Remove TextSelectionSection toggle component and link sheet wiring
- Add 'textSelection' to BetaFeatures type in featureFlags
- Fetch textSelection flag in all view page getStaticProps:
- pages/view/[linkId]/index.tsx (document + dataroom paths)
- pages/view/domains/[domain]/[slug]/index.tsx (document + dataroom)
- pages/view/[linkId]/d/[documentId].tsx (dataroom document)
- pages/view/domains/[domain]/[slug]/d/[documentId].tsx (domain dataroom doc)
- Pass textSelectionEnabled prop through component chain:
DocumentView -> ViewData -> NotionPage
DataroomDocumentView -> ViewData -> NotionPage
- Keep CSS override (.notion-text-selection-enabled) and NotionPage
conditional class application from previous commit
To enable: add the team's ID to the 'textSelection' array in
Vercel Edge Config betaFeatures.
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>