Compare commits

...
139 Commits
Author SHA1 Message Date
Hanzo Dev 5981dca439 ci(docker): reduce to sync-notice — native pipeline is .hanzo/workflows/deploy.yml
Image build+push+deploy now runs in-cluster (act_runner + BuildKit → operator).
GitHub is a mirror; this workflow is retained only as a manual sync notice.
2026-07-24 15:14:30 -07:00
Hanzo Dev af73ce21fe ci(deploy): add native Hanzo deploy pipeline (BuildKit → ghcr → operator patch)
Native flow: Hanzo Git push → act_runner → BuildKit builds Dockerfile →
ghcr.io/hanzoai/dataroom:<sha> → kubectl patch app dataroom → operator reconcile.
GitHub Actions reduced to sync-only.
2026-07-24 15:14:20 -07:00
1b4bb4830b feat(goja): self-contained goja bundle — fold dataroom into hanzoai/cloud (#101) (#6)
* feat(goja): self-contained goja bundle — fold dataroom into hanzoai/cloud (#101)

Extract the dataroom business logic into a self-contained, ESM-free goja bundle
(goja/bundle.js) exposing globalThis.handle(req), authored to run verbatim inside
the unified hanzoai/cloud binary (HIP-0106, task #101 / epic #96) — the same
in-process pattern @hanzo/plans and @hanzo/pricing use.

The complete flow — documents (metadata; bytes on the cloud object-storage seam),
data rooms + membership, shareable links (email/allow-list gate + bcrypt password
+ expiry + download toggle), viewers, per-page view analytics — is re-expressed as
a route table over injected host functions (globalThis.db.query/exec bound
per-tenant to Base/SQLite, globalThis.crypto bcrypt helpers). The Prisma models
become CREATE TABLE statements in the bundle's migrate route. No Postgres, no
Next.js; the standalone pod is retired.

cloud/clients/dataroom go:embeds a byte-identical vendored copy (task mandates
go:embed here since this repo is a TS app, not a Go module).

Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ

* goja: run on the shared clients/gojabase RW-Base binding (#101)

Rework the bundle onto the reusable clients/gojabase host contract (the RW-Base
goja binding captable pilots, esign reuses) instead of a bespoke DB binding — one
binding, not two. The bundle now calls __db.query/__db.exec, __newId, __now and
__bcrypt.hash/verify (the leaf's bcrypt HostFn), and handle({route,params,query,
orgId,body}); the per-tenant schema (DDL) moves to the Go leaf where gojabase runs
it on first open, and each dispatch is one per-tenant transaction (commits iff
status<400). No behaviour change to the dataroom flow; README updated.

Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ

---------

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-10 03:06:43 -07:00
0e31093a74 kv: harden in-process backend — atomic getdel, active expiry, multi-replica doc (#5)
Three RED-blessed non-blocking follow-ups on the KV-optional work.

1. Drop the non-atomic getdel() shim in lib/redis.ts. It shadowed BOTH backends'
   native atomic GETDEL with a get->del pipeline (other commands interleave
   between GET and DEL), reintroducing the one-time login-code double-use race
   that fetchAndDeleteLoginCodeData relies on GETDEL to prevent. ioredis maps
   getdel to the single-round-trip Redis GETDEL command; MemoryKV.getdel now
   reads+deletes in one un-yielded step (was `await this.get()` then delete — the
   await yielded the event loop, letting two racers both observe the value).

2. Bound MemoryKV growth. Lazy on-read eviction never fires for write-once-
   never-read keys (rl:<ip> rate-limit counters, one-shot tokens), so the map
   grew unbounded until the pod OOMKilled. Add sweepExpired() (one O(n) active-
   expire pass) run on an unref'd 60s timer, started by lib/redis.ts for the
   server singleton only (edge-gated; no test imports that module, so unit runs
   stay timer-free). MemoryKV stays pure (zero imports).

3. Document in .env.example that re-enabling multi-replica REQUIRES KV_URL
   (single-replica-by-construction otherwise: SQLite-on-RWO + in-process KV).
   The one-time non-silent in-process warning already exists in lib/kv/select.ts;
   no metrics surface exists, so log + doc is sufficient.

Tests: +3 cases (getdel atomicity under concurrent race; 100k-key sweep reclaims
to 0; sweep retains live/no-TTL keys). 28 pass, 3 skip (select.ts skips without
ioredis in a bare checkout), 0 fail.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-07 16:08:16 -07:00
4ed98334f0 KV-optional: run WITH and WITHOUT external Hanzo KV (#4)
* KV-optional: in-process backend when KV_URL unset

Make dataroom run WITH and WITHOUT external Hanzo KV, uniform with commerce
infra/kv.go. KV_URL unset -> in-process MemoryKV (single-replica correct: cache,
rate-limit, tus upload locks, export/download job stores, digest queues all work
with no external datastore). KV_URL set -> external Hanzo KV over ioredis
(multi-replica HA). Malformed KV_URL fails CLOSED (throws) -- never a silent
in-process fallback.

- lib/kv/url.ts: resolveKvUrl -- fail-closed parse, kv:// brand scheme -> redis://
  wire scheme, password redaction in errors.
- lib/kv/memory-kv.ts: in-process ioredis-compatible store (strings+TTL, zset,
  set, hash, list, pipeline) -- the exact surface dataroom uses.
- lib/kv/select.ts: the one backend selector (mirrors NewKVClient/FromURL).
- lib/redis.ts: use createKvClient() for redis + lockerRedisClient; drop the
  redis://localhost:6379 silent default (the WITHOUT-KV break) and vestigial
  REDIS_URL; Upstash-compat shims unchanged.
- Tests (node --test, dep-free): 25 pass proving selection + in-process
  correctness incl NX lock mutual-exclusion, TTL eviction, zset ordering,
  pipeline shape, fail-closed URL, cross-instance isolation (multi-replica
  split-brain contract).

* kv: warn once when falling back to in-process (non-silent split-brain)

Emit a one-time startup warning when KV_URL is unset so the WITHOUT-KV mode is
never silent — a multi-replica deploy with KV_URL unset splits sessions,
rate-limit windows and tus locks across replicas (single-replica only).

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-07 15:47:14 -07:00
zeekayandClaude Opus 4.8 9921a9a896 fix(db): re-inject Prisma enum objects for the SQLite client
SQLite has no Prisma enums, so removing the enum blocks also dropped the enum
*objects* the app imports from @prisma/client (LinkType.DOCUMENT_LINK, etc.),
crashing Next.js prerender ("Cannot read properties of undefined"). A
build-time step (after `prisma generate`) re-injects the 18 enum objects into
the generated client, so no app files change. Verified: local `next build`
now prerenders all 90 pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 00:59:54 -07:00
zeekayandClaude Opus 4.8 0aaaa1fa7e feat(db): migrate Prisma datasource postgres -> sqlite (native, file-backed)
Drops the shared sql-0 postgres dependency in favor of an embedded SQLite
file (DATABASE_URL=file:/data/<db>), WAL-replicated to SeaweedFS by the
operator persistence sidecar — same pattern as esign (Documenso).

Schema changes for the sqlite connector:
- datasource provider postgresql -> sqlite; drop directUrl/shadowDatabaseUrl
- 18 enums -> String (values preserved as string defaults)
- scalar-list fields (String[]/Int[]) -> Json @default("[]") (arrays
  round-trip through Prisma Json at runtime; next.config ignoreBuildErrors
  keeps the type-only churn out of the build)
- strip pg-native @db.* attributes (@db.Text/@db.Timestamp)
- startup: prisma migrate deploy -> prisma db push (pg migration history is
  postgres-specific; db push reconciles the sqlite schema idempotently)

CI: amd64-only (DOKS has no arm64); deploy:false — rollout is operator-managed
via the universe Service CR, never `kubectl set image`.

Verified: `prisma validate` + `prisma db push` materialize all 62 tables on a
fresh sqlite file. Source DB is empty (0 app rows), so cutover is data-safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 00:33:13 -07:00
2952083ef0 build: base on ghcr.io/hanzoai/nodejs:v24.18.0 (Node 24 + sqlite3)
Adopt the canonical Hanzo Node base image. Node 24 uniform across the
fleet; sqlite3 bundled (node:sqlite builtin + native better-sqlite3
toolchain). One base, one way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 12:49:21 -07:00
z af886c00a3 docs(brand): add hero banner 2026-06-28 20:09:09 -07:00
z 99203b41dd chore(brand): dynamic hero banner 2026-06-28 20:09:08 -07:00
Hanzo AI af5811b206 auth: use canonical /v1/iam/oauth/* IAM endpoints (bare /oauth/* hit SPA HTML) 2026-06-25 18:56:05 -07:00
7ec15f6af9 ci: run on self-hosted ARC pool (hanzo-build-linux-amd64/deploy), not GitHub-hosted (#3)
Co-authored-by: zeekay <z@hanzo.ai>
2026-06-19 20:33:48 -07:00
Antje WorringandClaude Opus 4.8 843791d1ee docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 10:12:19 -07:00
zeekay c404568baf ci: route to canonical native arcd labels [self-hosted, linux, <arch>]
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
2026-06-10 20:11:17 -07:00
Hanzo AI 4c1325cca9 ci: retrigger after orphan package deletion (auto-link on first push) 2026-06-08 12:42:42 -07:00
Hanzo AI 5f784733cf ci: retrigger after package linkage (image.source label set) 2026-06-08 09:36:07 -07:00
Hanzo AI 109e9eead2 ci: re-trigger after package claim + spark-hanzo label 2026-06-08 09:23:10 -07:00
Hanzo AI 2705155c2e ci: runner-arm64 → spark-hanzo (the label that's actually online)
The reusable workflow defaults runner-arm64 to spark-hanzoai-arm64,
but that label has zero online registrations on the hanzoai org
runner pool. Spark.local is registered as spark-hanzo (and
spark-hanzobot-arm64) — that's the canonical org-level pool label.
Builds queue forever otherwise.
2026-06-07 16:14:46 -07:00
Hanzo AI 31abfcc2da brand: real Hanzo mark + white-label env hooks
Replace the placeholder H badge with the canonical Hanzo mark (the
abstract H-shape from ~/work/hanzo/logo). Mark renders via
currentColor so it inherits whatever color the parent text sets —
no per-theme variants needed.

White-label is now fully driven by env:

  NEXT_PUBLIC_APP_NAME           full app name ('Hanzo Dataroom')
  NEXT_PUBLIC_APP_NAME_PRIMARY   first wordmark token (default: first
                                 word of APP_NAME)
  NEXT_PUBLIC_APP_NAME_SUFFIX    remainder (default: rest of APP_NAME)
  NEXT_PUBLIC_APP_TAGLINE        sub-line under the welcome headline
  NEXT_PUBLIC_IAM_PROVIDER_NAME  IAM brand for 'Sign in with X' button
  NEXT_PUBLIC_MARKETING_URL      base URL for Terms / Privacy links

Tenants drop in their own NEXT_PUBLIC_APP_NAME='Acme Rooms' and the
whole page reads correctly with no code changes — wordmark + welcome
copy + terms attribution all pick up automatically. The mark itself
stays Hanzo (this IS a Hanzo app); tenants that need their own glyph
swap the HanzoMark import for a custom <img src=BRAND_LOGO_URL />.

Favicons regenerated from the canonical SVG too.
2026-06-07 16:01:01 -07:00
Hanzo AI 617480df3a ui: dark + monochrome + clean centered IAM signin
Login was a split-pane marketing layout with a salmon-pink sign-in
button, light-gray panel, testimonial photo + quote, and 'Trusted by
teams at' sponsor wall. None of that is wanted on a Hanzo property.

Now:
  * Solid black full-screen background, no testimonial panel, no
    sponsor logos.
  * Centered logo + 'Welcome to Hanzo Dataroom' headline + tagline.
  * Monochrome white-on-black 'Sign in with Hanzo' button (bg-white,
    text-black). No red.
  * Terms/Privacy links in zinc-300 underline, body in zinc-400/500.
  * <html class='dark'> on the root layout — dark is the default theme.
  * Favicon swapped to a Hanzo-H mark across all favicon sizes.

The IAM redirect itself is unchanged — signIn('hanzo-iam') still
hands off to NEXT_PUBLIC_IAM_PROVIDER_NAME via NextAuth, so 'Sign in
with Hanzo' is now the only UI surface and IAM owns everything past
the redirect.
2026-06-07 13:20:36 -07:00
Hanzo AI 2cf7f9503e polish: text-based Hanzo Dataroom logo + fix duplicated 'Data Rooms' suffix
Visual cleanups verified via Playwright after first dev run:

  * Replaced the three logo SVGs (light, dark, mark) with text-based
    placeholders that read 'Hanzo Dataroom' / 'H' instead of the
    original Papermark vector wordmarks. Local dev now reads correctly
    in the auth flow; real artwork can drop in by overwriting the
    same files.

  * Removed 'Data Rooms' duplication from the rebrand-sweep
    artifacts. The original copy reads '<brand> Data Rooms' which
    became 'Hanzo Dataroom Data Rooms' after the naive
    Papermark→Hanzo Dataroom substitution. Fixed across:
      - app/(auth)/login/page-client.tsx testimonial
      - app/(auth)/auth/email/[[...params]]/page-client.tsx
      - components/emails/data-rooms-information.tsx
      - components/emails/dataroom-trial-24h.tsx
      - components/welcome/dataroom-trial.tsx
2026-06-07 12:32:31 -07:00
Hanzo AI b827b3a7bd rebrand: Papermark → Hanzo Dataroom across user-facing surfaces
108 files: README, marketing copy, page titles, meta tags, footer
text, email templates, .env.example, UI strings. URLs swapped
from papermark.com/papermark.io to dataroom.hanzo.ai.

Internal identifiers left intact per repo policy: localStorage
keys (papermark.email/papermark.name/last_papermark_login —
changing them logs users out), type field names
(papermarkUserId), internal helper symbols (PAPERMARK_HEADERS,
PapermarkSparkle component, isPapermarkUrl helper),
internal-only filenames. LLM.md retains 'Upstream: Papermark'
attribution line. Per-locale .po translations regenerate from
source on the next lingui-extract CI run.

DB migrations untouched (append-only history; rewriting them
breaks installed schemas). LICENSE stays AGPL-3.0.
2026-06-07 12:23:40 -07:00
Hanzo AI d4f4071ee4 rip: enterprise dirs + paywalls — full AGPL, no commercial-license code
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.
2026-06-07 11:46:48 -07:00
Hanzo AI caf8fa1053 ci: build amd64 + arm64 natively on hanzo runner pools
Reverts the amd64-only narrowing and restores the canonical
multi-arch build per Hanzo's runner topology (RUNNERS.md):
- amd64 leg targets hanzo-build-linux-amd64 (DOKS ARC pool on
  hanzo-k8s, default in the shared docker-build.yml — no caller
  override needed).
- arm64 leg targets spark-hanzoai-arm64 (Ampere bare metal,
  also default).

Both are native — no QEMU emulation. arm64 jobs queue while
spark arcd is offline; once spark is online (or an equivalent
arm64 ARC pool is installed somewhere reachable) the queue
drains automatically.
2026-06-07 11:46:03 -07:00
Hanzo AI 7aafdf2b65 ci: amd64-only build (matches hanzo runner pool topology)
hanzo-build-linux-amd64 is the only ARC runner pool installed on
hanzo-k8s today (arm64 pool is paused until DigitalOcean ships
arm64 droplets, per ~/work/hanzo/.github/RUNNERS.md). The arm64
job was waiting on spark-hanzoai-arm64 — not provisioned in this
cluster — so every build sat in queue forever and the amd64 leg
got cancelled.

Set platforms: linux/amd64 on the reusable build call to skip
arm64 entirely. Re-add arm64 if/when DO ships arm64 droplets and
the spark runner pool comes back online.
2026-06-07 11:38:49 -07:00
Hanzo AI d568e189a0 vendor: handsontable 6.2.2 full.min.css for excel-viewer build
components/view/viewer/excel-viewer.tsx imports
'@/public/vendor/handsontable/handsontable.full.min.css' at
build time, but the file was never committed — every Docker build
failed at the webpack module-resolution step.

The companion JS loads via CDN at runtime
(cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js).
Pin the CSS to the same version, vendor it in, and the build
clears.
2026-06-07 11:19:56 -07:00
Hanzo AI d21548746c Merge remote-tracking branch 'origin/main' 2026-06-01 16:18:03 -07:00
Hanzo AI d5a58f710c merge: ci/canonical-docker-build-1776995235 2026-06-01 16:18:02 -07:00
Hanzo AI 77799ddce2 chore: rip stripe → @hanzoai/commerce + @hanzoai/pay (no stripe period)
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.
2026-05-28 02:03:38 -07:00
Hanzo AI dc592aa79d docs(llm): explicit Upstream attribution line
LICENSE-attested or repo-attested upstream is now called out at the
top of LLM.md alongside the project description, so downstream
audits (universe/docs/PRODUCTS.md) can rely on a single canonical
location for the OSS lineage statement.
2026-05-18 21:35:47 -07:00
Hanzo AI f99ecc8e90 ci: add id-token: write to caller permissions
Required for hanzoai/.github/.github/workflows/docker-build.yml@main —
without it the workflow_call dies as startup_failure with no jobs
dispatched. Caller permissions are a CEILING.
2026-05-07 09:09:52 -07:00
Hanzo DevandGitHub 30cdcf7339 ci: migrate to canonical hanzoai/.github/docker-build.yml reusable (#2) 2026-04-23 18:53:36 -07:00
Hanzo AI c1e7842cca ci: migrate to canonical hanzoai/.github/docker-build.yml reusable 2026-04-23 18:48:09 -07:00
Hanzo DevandGitHub 978f6c5e88 ci: migrate to canonical docker-build reusable workflow (#1)
Replace bespoke build-and-push + universe-reusable-deploy-service pair
with the canonical caller that does both in a single workflow:
hanzoai/.github/.github/workflows/docker-build.yml@main.

Native amd64+arm64 ARC runners, semver + branch tags, multi-arch manifest,
kubectl rollout restart on main after push.

Co-authored-by: Hanzo AI <dev@hanzo.ai>
2026-04-23 18:12:23 -07:00
Hanzo AI 9ccb0508b4 chore: add .dockerignore 2026-04-17 16:58:13 -07:00
Hanzo AI 54c79e2b6f chore: symlink AGENTS.md and CLAUDE.md to LLM.md
Canonical project context lives in LLM.md. Symlinks ensure
agentic coding tools (Claude Code, Cursor, etc.) find context
automatically regardless of which filename they look for.
2026-04-01 14:11:20 -07:00
Hanzo AI a6b5d4c570 chore: remove vendored handsontable files 2026-03-03 14:00:00 -08:00
Hanzo Dev 19cce6e851 fix: remove tracked vendor dir, add to gitignore 2026-03-28 18:13:20 -07:00
Hanzo Dev c8284f3f27 fix: use published @hanzo/insights package directly 2026-03-14 01:36:35 -07:00
Hanzo Dev 1ad91d413e rebrand: zero posthog, no compat
Rename x-posthog-* header pattern to x-insights-* in
analytics middleware.
2026-03-13 20:33:42 -07:00
Hanzo Dev 263e4f6c32 rebrand: use direct Insights imports, no compat aliases 2026-03-13 20:15:26 -07:00
Hanzo Dev e7d7e6aeac rebrand: final PostHog purge 2026-03-13 17:54:38 -07:00
Hanzo Dev 1408d08f38 rebrand: purge PostHog references 2026-03-13 17:38:17 -07:00
Hanzo Dev 3ccb6035b5 rebrand: purge remaining PostHog references 2026-03-13 17:11:13 -07:00
Hanzo Dev eb97698382 ci: migrate deploy to HANZO_API_KEY + KMS via reusable workflow
Replace direct DIGITALOCEAN_ACCESS_TOKEN with reusable-deploy-service.yml
from hanzoai/universe that fetches credentials from KMS using HANZO_API_KEY.
2026-03-11 14:35:00 -07:00
Hanzo Dev c3817a2e0a docs: add LLM.md project guide 2026-03-11 10:29:51 -07:00
Hanzo Dev ddcfc62f54 chore: rename HANZO_IAM_ env vars to IAM_ prefix
Drop HANZO_ prefix from all IAM environment variable names for
consistency across the Hanzo ecosystem.
2026-03-10 16:31:45 -07:00
Hanzo Dev 1244cb74f6 feat: add org_id claim from IAM to JWT/session, document IAM env vars
- 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
2026-03-09 22:27:13 -07:00
Hanzo Dev 3867fa934d fix: update auth pages and branding for Hanzo IAM 2026-03-03 12:13:13 -08:00
Hanzo Dev 880f522457 feat: IAM-only auth
Remove email, Google, LinkedIn, passkey, SAML auth from login —
only allow Sign in with Hanzo (IAM).
2026-03-02 23:45:00 -08:00
Hanzo Dev 2724995f04 fix: copy entire node_modules/.bin to include Prisma WASM files
The Prisma CLI binary references companion .wasm files
(prisma_schema_build_bg.wasm) that live alongside it in .bin/.
Copying only the prisma binary missed these files.
2026-03-01 19:47:13 -08:00
Hanzo Dev 513f0c4781 fix: repair broken function names from branding sed replacement
The mass sed replacement of 'Papermark' → 'Hanzo Dataroom' broke
function names that contained 'Papermark' as part of a camelCase
identifier (e.g. PapermarkSparkle → Hanzo DataroomSparkle).
2026-03-01 19:20:32 -08:00
Hanzo Dev 126976b33f Rebrand from Papermark to Hanzo Dataroom
- Replace all Papermark branding with Hanzo Dataroom
- Update metadata, titles, descriptions, OG tags
- Update email templates footer and branding
- Replace papermark.io/com URLs with dataroom.hanzo.ai
- Update powered-by component
- Update x-powered-by header
- Update legal references to Hanzo AI, Inc.
- Update social handles to @hanzoai
- Update README, CLA, LICENSE, SECURITY docs
2026-03-01 19:02:19 -08:00
Hanzo Dev 2a6073e5c2 Fix Prisma migrations: symlink schema/migrations to prisma/migrations
Prisma multi-file schema at prisma/schema/schema.prisma looks for
migrations at prisma/schema/migrations/ by default. Actual migrations
are at prisma/migrations/. Symlink bridges the two.
2026-03-01 09:09:58 -08:00
Hanzo Dev 1e91e2b50a Fix Edge Runtime crash: remove ioredis from middleware bundle
- 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
2026-03-01 08:33:05 -08:00
Hanzo Dev 8b57b27eb0 Replace Upstash with Hanzo KV (ioredis wire-compatible client)
- Remove @upstash/redis, @upstash/ratelimit, @upstash/qstash
- Add ioredis for Hanzo KV wire protocol compatibility
- Rewrite lib/redis.ts with Upstash-compatible shims over ioredis
- Rewrite tus-redis-locker.ts to use ioredis types
- Replace @upstash/ratelimit with sliding-window impl over KV
- Stub qstash in lib/cron (not used in production)
- Env: KV_URL replaces UPSTASH_REDIS_REST_URL
2026-03-01 08:09:53 -08:00
Hanzo Dev 10473cb76f fix: use correct prisma schema path and bundle prisma CLI in runner
Schema is at prisma/schema/schema.prisma (multi-file schema).
Bundle prisma CLI to avoid slow npx download on every pod start.
2026-03-01 07:44:03 -08:00
Hanzo Dev b2d3952309 fix: wrap useSearchParams pages in Suspense for Next.js build
Pages using useSearchParams() in client components need a Suspense
boundary to avoid build errors during static page generation.
2026-03-01 07:29:43 -08:00
Hanzo Dev 0590fd306e fix: increase Node heap size for Docker build
QEMU cross-compilation and large bundles cause OOM during Next.js build.
2026-03-01 07:08:09 -08:00
Hanzo Dev 1dd9d0eb42 fix: remove constructor throw in SlackClient to prevent build crash
Next.js collects page data during build, importing modules at module scope.
SlackEventManager instantiates SlackClient which threw when SLACK_CLIENT_ID
was unset, crashing the build at /api/views-dataroom page collection.
2026-03-01 06:45:58 -08:00
Hanzo Dev b4aefb200e fix: add dummy env vars during Docker build for module-scope inits
OpenAI and Hanko clients initialize at module scope, crashing Next.js
build when collecting page data. Set dummy values only for build stage.
2026-03-01 06:36:41 -08:00
Hanzo Dev 6800768d0b fix: make hanko passkey provider optional for builds without env vars
Return null from getHanko() when HANKO_API_KEY is unset, and
conditionally include PasskeyProvider in NextAuth only when configured.
2026-03-01 06:31:32 -08:00
Hanzo Dev b8eb58d801 fix: lazy-init hanko client to prevent build-time crash
Hanko client was throwing at module scope when HANKO_API_KEY was unset.
Convert to lazy getter so it only initializes at runtime.
2026-03-01 06:23:35 -08:00
Hanzo Dev cbf081c0b2 fix: async searchParams for invitation page + skip TS build errors
Fix Next.js 15 async searchParams in invitation page and add
typescript.ignoreBuildErrors as safety net for remaining pages.
2026-03-01 06:16:29 -08:00
Hanzo Dev 639c52d9ab fix: use async params for Next.js 15 compatibility
Next.js 15 requires page params to be Promise<T> instead of plain objects.
2026-03-01 06:09:30 -08:00
Hanzo Dev 085f635cc4 Fix build: add defaults for env vars used in Next.js config 2026-03-01 06:01:48 -08:00
Hanzo Dev baba5bbaa9 Use npm install instead of npm ci in Dockerfile 2026-03-01 05:54:51 -08:00
Hanzo Dev e426826edc Regenerate package-lock.json with override resolution 2026-03-01 05:53:44 -08:00
Hanzo Dev 583b7667be Fix Dockerfile: add build deps for native modules 2026-03-01 05:51:35 -08:00
Hanzo Dev ce60abfbce Add Dockerfile and CI/CD for K8s deployment
- Add Dockerfile with standalone Next.js output
- Add output: "standalone" to next.config.mjs (DOCKER_OUTPUT env)
- Add GitHub Actions workflow for GHCR build + K8s deploy
2026-03-01 05:48:20 -08:00
Hanzo Dev effa28bdb5 Upgrade to Next.js 15, TypeScript 5.9
- next: 14.2.35 → 15.1.0
- typescript: 5 → 5.9.3

React 18, next-auth 4.x, Prisma 6.x kept as-is.
2026-03-01 03:09:52 -08:00
Hanzo Dev 53c72672ca feat: add Hanzo IAM OIDC SSO provider
- Add HanzoIAMProvider to NextAuth config (OIDC with userinfo endpoint)
- Add "Continue with Hanzo" button to login page (first SSO option)
- Env vars: HANZO_IAM_URL, HANZO_IAM_CLIENT_ID, HANZO_IAM_CLIENT_SECRET
- IAM app registered as app-dataroom in init_data.json
2026-03-01 22:36:44 -08:00
Marc SeitzandGitHub 2e30946472 Merge pull request #2103 from mfts/cursor/duplicated-link-properties-b1a0
Duplicated link properties
2026-03-06 22:28:12 +11:00
Marc SeitzandGitHub f31e261811 Merge pull request #2102 from mfts/codex/agent
feat(ee): improve agents
2026-03-05 23:05:12 +11:00
Cursor AgentandMarc Seitz 5168a80b30 fix: duplicate custom fields and visitor groups when duplicating a link
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>
2026-03-05 10:47:16 +00:00
Marc Seitz bfc02040ca fix: limit source files 2026-03-05 21:42:34 +11:00
Marc Seitz 1e65b74b8d Merge branch 'codex/agent' of github.com:mfts/papermark into codex/agent 2026-03-05 17:58:56 +11:00
Marc Seitz a0f91e4b93 fix 2026-03-05 17:58:34 +11:00
Marc SeitzGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
6ead6e6410 Update ee/features/ai/components/chat-message.tsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-05 17:49:36 +11:00
Marc SeitzGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
0df4ab5a74 Potential fix for code scanning alert no. 92: Incomplete string escaping or encoding
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-05 17:47:52 +11:00
Marc Seitz afa7e0fa7d feat(ee): improve agents 2026-03-05 17:38:18 +11:00
Marc SeitzandGitHub df0f8f612f Merge pull request #2100 from mfts/cursor/docx-sanitizer-additions-a1a1
Docx sanitizer additions
2026-03-05 17:29:17 +11:00
Cursor AgentandMarc Seitz fd68e10379 Replace regex-based pPr extraction with XML tree parsing in NUMPAGES fix
Remove PPR_RE, PPR_BLOCK_RE, and PARA_RE regex constants that were
used to extract <w:pPr> from paragraph fragments via non-greedy
matching. Replace with proper xml.etree.ElementTree parsing of the
entire header/footer file:

- _register_all_namespaces() preserves original namespace prefixes
  during ET serialization
- strip_numpages_fields_in_hf() now iterates the parsed XML tree to
  find <w:p> elements with NUMPAGES/SECTIONPAGES instrText, removes
  all children except <w:pPr> (found via Element.find), and writes
  the cleaned tree back

This handles arbitrarily nested pPr structures safely and avoids
regex truncation on malformed documents.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-05 06:17:40 +00:00
Cursor AgentandMarc Seitz 19e93b589e Preserve original paragraph properties in NUMPAGES field fix
Instead of hardcoding a Footer style on replacement paragraphs,
extract the existing <w:pPr> block (or self-closing tag) from the
original paragraph and reuse it. This avoids style mismatches in
headers/footers that use custom paragraph properties.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-05 05:57:43 +00:00
Marc SeitzandGitHub bd1e3c04f3 Merge pull request #2101 from mfts/chore/deps
chore: update dependencies
2026-03-05 16:55:58 +11:00
Marc Seitz c78e97962b fix 2026-03-05 16:48:43 +11:00
Marc Seitz 2aadaf8888 chore: update dependencies 2026-03-05 16:22:48 +11:00
Cursor AgentandMarc Seitz 797d470468 Add NUMPAGES/SECTIONPAGES footer field fix to DOCX sanitizer
Strip nested IF/NUMPAGES field codes from headers/footers that cause
infinite layout recalculation loops in LibreOffice's UNO API
(loadComponentFromURL hangs). Paragraphs containing NUMPAGES or
SECTIONPAGES instrText are replaced with empty paragraphs to break
the loop. This fix runs in 'all' mode alongside existing RTL compat,
glossary removal, and SDT unwrap fixes.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-05 05:19:26 +00:00
Marc SeitzandGitHub 120a1b88fb Merge pull request #2083 from mfts/cursor/root-domain-redirects-96b8
Root domain redirects
2026-03-05 14:33:58 +11:00
Marc SeitzandGitHub 86b0ee68b9 Merge pull request #2095 from mfts/cursor/upload-limit-warnings-9750
Upload limit warnings
2026-03-02 18:49:50 +11:00
Marc Seitz a44bae5edd fix: fix propagate button 2026-03-02 18:31:27 +11:00
Marc Seitz 6950dec056 chore: prettier 2026-03-02 18:21:53 +11:00
Marc Seitz 6e9d6fe7d4 feat: force https on redirect url 2026-03-02 18:21:42 +11:00
Marc Seitz 5e115f1896 refactor: remove outdated getTeamWithDomain helper 2026-03-02 18:21:31 +11:00
Marc Seitz 4b557e909c Merge branch 'main' into cursor/root-domain-redirects-96b8 2026-03-02 17:56:26 +11:00
Marc Seitz 8c3d677af8 Merge branch 'main' into cursor/upload-limit-warnings-9750 2026-03-02 17:46:24 +11:00
Marc SeitzandGitHub 6687d842db Merge pull request #2096 from mfts/cursor/file-upload-authentication-bypass-dcd1
File upload authentication bypass
2026-03-02 17:43:12 +11:00
Marc SeitzandGitHub 4a969ad9ad Merge pull request #2093 from mfts/feat/dataroom-visitor-notifications
feat: add notification settings for visitors
2026-03-02 17:27:44 +11:00
Cursor AgentandMarc Seitz 0b1acdf136 Cap folder traversal at remainingDocuments to prevent unnecessary backend folder creation
getFilesFromEvent now tracks a collectedFileCount counter and a fileLimit
derived from remainingDocuments. traverseFolder checks the counter before
entering directories (skipping folder creation) and before resolving files.
Once the budget is exhausted the traversal short-circuits, so no extra API
calls are made for folders that would never receive uploads.

A fileLimitTruncatedRef signals to onDrop that files were capped during
traversal so the warning toast fires even though acceptedFiles.length is
already <= remainingDocuments. The else-if branch in onDrop still handles
the file-picker path (no traversal) as a safety net.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-02 06:23:49 +00:00
Marc Seitz 13a98ca7e5 fix: don't send empty digest 2026-03-02 17:23:23 +11:00
Cursor AgentandMarc Seitz 015c44af80 Harden TUS upload auth, team access, and limits
Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-02 06:11:09 +00:00
Marc SeitzandGitHub 1ebfd522a3 Merge pull request #2094 from mfts/cursor/q-a-content-wrapping-64f3
Q&a content wrapping
2026-03-02 16:59:14 +11:00
Cursor AgentandMarc Seitz 82b47a3e76 fix: increase Q&A message limit to 4000 chars and surface validation errors
The admin was unable to reply to conversations because the 1000 character
limit was too restrictive for dataroom Q&A. This commit:

- Increases the default message character limit from 1000 to 4000
- Surfaces the actual validation error message (e.g. 'Content cannot be
  longer than 4000 characters') through the API instead of returning a
  generic 'Internal server error'
- Parses and displays the API error in the frontend toast notification
- Adds a character counter to the admin reply form with visual feedback
  when approaching/exceeding the limit
- Adds maxLength to viewer-side input fields for client-side enforcement

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-02 05:08:58 +00:00
Cursor AgentandMarc Seitz 0f59913c14 fix: Q&A content text wrapping so questions and answers are not cut off
- conversation-message.tsx: Change w-max to w-fit on message bubbles so
  text wraps at max-w-[80%] instead of overflowing. Add min-w-0 and
  break-words to the content paragraph for proper word wrapping.

- faq-section.tsx: Add min-w-0 and break-words to FAQ answer text to
  prevent overflow in flex containers. Add break-words to question text
  and min-w-0 to accordion trigger for consistent wrapping behavior.

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-02 04:59:51 +00:00
Marc Seitz d5c7a6d109 fix 2026-03-02 15:05:35 +11:00
Marc Seitz c394f623eb fix: try and catch around sending notification 2026-03-02 13:58:19 +11:00
Marc Seitz adde340bbd fix: don't ignore other keys in notificationprefs 2026-03-02 13:13:16 +11:00
Marc Seitz 32275c6068 fix: wrap in try catch 2026-03-02 13:13:04 +11:00
Marc Seitz ef2f06f824 fix: throw if no linkurl present 2026-03-02 13:12:54 +11:00
Marc Seitz 8d5568b807 fix: classname typo 2026-03-02 13:12:39 +11:00
Marc Seitz eaabf16b2c fix: type error 2026-03-02 12:40:58 +11:00
Marc Seitz 3c7e4f93d0 feat: add notification settings for visitors 2026-03-02 12:31:44 +11:00
Marc SeitzandGitHub 3f95f1c118 Merge pull request #2092 from mfts/feat/deps
fix: remove warning
2026-03-02 12:20:05 +11:00
Marc Seitz 996a40ceb9 fix: remove warning 2026-03-01 13:33:15 +11:00
Marc SeitzandGitHub 6c1f5e261d Merge pull request #2091 from mfts/chore/robots
chore: update robots
2026-02-28 17:11:50 +11:00
Marc Seitz dc0de8a8dd chore: update robots 2026-02-28 16:58:45 +11:00
Marc Seitz 898313f5a5 fix: remove redirect if it fails 2026-02-28 15:29:32 +11:00
Marc Seitz a279392ba9 chore: make sure keyword matching is case insentive 2026-02-28 15:24:01 +11:00
Marc Seitz 6951b2d92a feat: add zod validation for domain update body 2026-02-28 15:23:35 +11:00
Marc Seitz 76444e2992 Merge branch 'main' into cursor/root-domain-redirects-96b8 2026-02-28 15:20:12 +11:00
Marc SeitzandGitHub 47c256a53c Merge pull request #2004 from mfts/cursor/PM-468-dataroom-upload-visibility-b14f
Dataroom upload visibility
2026-02-28 08:38:33 +11:00
Marc Seitz a0570a18ca fix: colors for uploads 2026-02-28 08:23:19 +11:00
Marc Seitz 58d31905ef feat: successive uploads 2026-02-28 08:23:12 +11:00
Marc Seitz b8c6bb093f Merge branch 'main' into cursor/PM-468-dataroom-upload-visibility-b14f 2026-02-27 19:05:29 +11:00
Marc SeitzandGitHub 24b22dc1df Merge pull request #2041 from mfts/deal-flow-infp
Deal flow info
2026-02-27 17:50:49 +11:00
Marc Seitz d3bd1802cd fix: return early if no document present 2026-02-27 16:35:46 +11:00
Marc Seitz 3ae1dbb678 fix: clear uploads and uploadids 2026-02-27 16:33:43 +11:00
Marc Seitz 58476317a5 refactor: extract duplicate filter logic 2026-02-27 16:33:28 +11:00
Marc Seitz c670f29273 feat: reset rejected file 2026-02-27 16:30:17 +11:00
Cursor AgentandMarc Seitz 0dd914a87d Add pre-upload validation warnings for document and bulk upload limits
- Handle 'too-many-files' rejection in onDropRejected with clear message
  showing the max files per upload limit and suggesting smaller batches
- Improve onDrop document limit check: show usage (X/Y) and upgrade action
  when at limit, and truncate files with warning when partially exceeding
- Add early return in getFilesFromEvent to skip folder creation when at limit
- Improve add-document-modal error messages with usage context and upgrade CTA
- Fix remainingDocuments calculation: use Infinity for unlimited plans

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-02-26 14:54:47 +00:00
Marc SeitzandCursor 53d328c2d9 fix: resolve three bugs in upload visibility feature
- Fix persisted uploads stuck in 'processing': updatePendingUpload now
  updates both in-flight and persisted state arrays
- Fix duplicate filename collision: key pending upload IDs by batch index
  instead of filename, thread index through onUploadComplete callback
- Fix modal premature close on multi-file upload: track expected/completed
  counts and only fire onUploadSuccess after all files resolve

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:30:15 +11:00
Marc SeitzandCursor 5f81333e6e feat: add dataroom upload visibility with optimistic UI and persisted uploads
- Add GET endpoint to fetch viewer's previously uploaded documents
- Return document data from POST upload for optimistic UI rendering
- Add PendingUploadsProvider context with server-side persistence
- Add PendingDocumentCard with Trigger.dev realtime processing status
- Add Documents/My Uploads tab switcher in dataroom viewer
- Redesign upload modal with folder indicator and success state
- Redesign upload component with progress bars and drag-drop zone

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 18:30:08 +11:00
Marc Seitz cf1e783d8f Merge branch 'main' into cursor/PM-468-dataroom-upload-visibility-b14f 2026-02-23 13:19:45 +11:00
Cursor AgentandMarc Seitz ce2b40daad fix: harden root domain redirects - validation, plan gating, cleanup
- 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>
2026-02-22 08:24:29 +00:00
Cursor AgentandMarc Seitz 2709049f68 feat: add UI for root domain redirect and Redis sync endpoint
- Update DomainCard to accept and display redirectUrl with inline editor
- Add save/remove functionality for redirect URL with toast notifications
- Pass redirectUrl from domains settings page to DomainCard
- Add /api/cron/domains/sync-redirects endpoint to sync Postgres -> Redis
- Add backfill SQL for existing hardcoded domain redirects

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-02-20 22:50:09 +00:00
Cursor AgentandMarc Seitz e81d4118aa feat: add root domain redirect support (schema, redis, middleware, API)
- 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>
2026-02-20 22:45:48 +00:00
Marc Seitz 5d041f20f8 Merge branch 'main' into cursor/PM-468-dataroom-upload-visibility-b14f 2026-02-12 16:49:10 +11:00
Marc Seitz 053394901c Merge branch 'main' into cursor/PM-468-dataroom-upload-visibility-b14f 2026-01-28 12:24:04 +11:00
Cursor Agentandmarcftone 5e7ff9fa29 feat: show uploaded documents immediately with processing state
- Add PendingUploadsContext for managing optimistic uploads
- Create PendingDocumentCard component to show uploading/processing documents
- Update upload API to return document data for optimistic display
- Update ViewerUploadComponent to track pending uploads
- Update DataroomViewer to display pending uploads at top of list
- Update DocumentUploadModal with success feedback and auto-close
- Add 'pending' prefix to id-helper for generating pending upload IDs

This ensures external visitors see their uploaded documents immediately
after upload, with a clear 'Processing...' indicator while the document
is being processed on the backend.

Fixes PM-468

Co-authored-by: marcftone <marcftone@gmail.com>
2026-01-21 10:33:38 +00:00
571 changed files with 15184 additions and 10912 deletions
+13
View File
@@ -0,0 +1,13 @@
node_modules
**/node_modules
**/.next
.git
.env*
.vscode/
.idea/
coverage/
*.test.ts
*.spec.ts
.DS_Store
*.md
docs/
+28 -5
View File
@@ -12,9 +12,15 @@ POSTGRES_PRISMA_URL_NON_POOLING=
# This variable is from Vercel Storage Blob
BLOB_READ_WRITE_TOKEN=
# Google client id and secret for authentication
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Hanzo IAM OAuth (required for production)
IAM_URL="https://hanzo.id"
IAM_CLIENT_ID=""
IAM_CLIENT_SECRET=""
# IAM_PROVIDER_NAME="Hanzo"
# Google client id and secret (deprecated — use Hanzo IAM above)
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# This variable is from Resend to send emails
RESEND_API_KEY=
@@ -67,8 +73,25 @@ NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS=
# Encryption key for document passwords.
NEXT_PRIVATE_DOCUMENT_PASSWORD_KEY=my-superstrong-document-secret
# [[REDIS LOCKER CONFIGURATION]]
# For bulk upload using tus.io, we use a Redis-based locker to prevent corruption of the data.
# [[HANZO KV]] — OPTIONAL. Leave UNSET to run on the in-process backend
# (single-replica correct: cache, rate-limit, tus upload locks, export/download
# job stores and digest queues all work with no external datastore). SET it to
# an external Hanzo KV instance for multi-replica HA (shared state across pods).
# Accepts the Hanzo KV brand scheme kv:// (kvs:// for TLS) or a redis:// DSN.
# A malformed value fails CLOSED (the app throws rather than silently degrade).
#
# Re-enabling multi-replica (replicas > 1) REQUIRES KV_URL. dataroom is
# single-replica-by-construction otherwise: SQLite on a ReadWriteOnce volume plus
# the in-process KV. Without KV_URL each pod owns its OWN map, so sessions,
# rate-limit windows and tus upload locks split-brain across replicas — scaling
# out needs a shared DB AND KV_URL set.
# KV_URL=kv://:password@hanzo-kv:6379
KV_URL=
# [[TUS UPLOAD LOCKER]] — for bulk upload via tus.io, an exclusive locker
# prevents data corruption. It uses the Hanzo KV client above (KV_URL); with
# KV_URL unset the lock is in-process (correct for a single replica). The legacy
# Upstash REST locker below is unused and kept only for reference.
UPSTASH_REDIS_REST_LOCKER_URL=
UPSTASH_REDIS_REST_LOCKER_TOKEN=
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="dataroom">
<rect width="1280" height="640" fill="#0A0A0A"/>
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">dataroom</text>
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Papermark is the open-source DocSend alternative with built-in…</text>
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+3 -3
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
CLAAssistant:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
@@ -25,10 +25,10 @@ jobs:
# This token is required only if you have configured to store the signatures in a remote repository/organization
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
remote-organization-name: 'papermark'
remote-organization-name: 'hanzo-dataroom'
remote-repository-name: 'cla-signatures'
path-to-signatures: 'signatures/version1/cla.json'
path-to-document: 'https://github.com/mfts/papermark/blob/main/CLA.md'
path-to-document: 'https://github.com/hanzoai/dataroom/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist: cursoragent
+11
View File
@@ -0,0 +1,11 @@
name: Docker
# Native deploy pipeline is .hanzo/workflows/deploy.yml (Hanzo Git → act_runner →
# BuildKit → ghcr.io/hanzoai/dataroom:<sha> → operator reconcile → hanzocd).
# GitHub is a mirror; this workflow is retained only as a manual sync notice.
on:
workflow_dispatch:
jobs:
notice:
runs-on: ubuntu-latest
steps:
- run: echo "native pipeline is .hanzo/workflows/deploy.yml; GitHub is a mirror"
+7
View File
@@ -0,0 +1,7 @@
name: Workflow Sanity
on:
pull_request:
paths: ['.github/workflows/**']
jobs:
sanity:
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
+1 -1
View File
@@ -62,4 +62,4 @@ lib/emails/marketing
# changelog and docs
changelog
.docs
.docsvendor/
+24
View File
@@ -0,0 +1,24 @@
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: hanzo-linux-amd64
steps:
- uses: actions/checkout@v4
- name: Build + push image
run: |
SHA="${GITHUB_SHA::8}"
buildctl-daemonless.sh build --frontend=dockerfile.v0 \
--opt context="${{ github.server_url }}/${{ github.repository }}.git#${GITHUB_SHA}" \
--opt filename=Dockerfile --opt platform=linux/amd64 \
--secret id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN \
--output "type=image,name=ghcr.io/hanzoai/dataroom:${SHA},push=true" --progress=plain
env:
GIT_AUTH_TOKEN: ${{ secrets.GIT_CLONE_TOKEN }}
- name: Deploy — declare tag to operator
run: |
for app in dataroom; do
kubectl -n hanzo patch app "$app" --type=merge -p "{\"spec\":{\"image\":{\"repository\":\"ghcr.io/hanzoai/dataroom\",\"tag\":\"${GITHUB_SHA::8}\"}}}"
done
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+8 -8
View File
@@ -1,16 +1,16 @@
# Papermark Contributors License Agreement
# Hanzo Dataroom Contributors License Agreement
This Contributors License Agreement ("CLA") is entered into between the Contributor, and Papermark, Inc. ("Papermark"), collectively referred to as the "Parties."
This Contributors License Agreement ("CLA") is entered into between the Contributor, and Hanzo AI, Inc. ("Hanzo Dataroom"), collectively referred to as the "Parties."
## Background:
Papermark is an open-source project aimed at providing an open-source document sharing and tracking infrastructure for all parties. This CLA governs the rights and contributions made by the Contributor to the Papermark project.
Hanzo Dataroom is an open-source project aimed at providing an open-source document sharing and tracking infrastructure for all parties. This CLA governs the rights and contributions made by the Contributor to the Hanzo Dataroom project.
## Agreement:
**Contributor Grant of License:**
By submitting code, documentation, or any other materials (collectively, "Contributions") to the Papermark project, the Contributor grants Papermark a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the Papermark project.
By submitting code, documentation, or any other materials (collectively, "Contributions") to the Hanzo Dataroom project, the Contributor grants Hanzo Dataroom a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the Hanzo Dataroom project.
**Representation of Ownership and Right to Contribute:**
@@ -18,11 +18,11 @@ The Contributor represents that they have the legal right to grant the license s
**Patent Grant:**
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant Papermark a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the Papermark project.
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant Hanzo Dataroom a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the Hanzo Dataroom project.
**No Implied Warranties or Support:**
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. Papermark shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. Hanzo Dataroom shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
**Retention of Contributor Rights:**
@@ -38,8 +38,8 @@ This CLA constitutes the entire agreement between the Parties with respect to th
**Acceptance:**
By submitting Contributions to the Papermark project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
By submitting Contributions to the Hanzo Dataroom project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
**Effective Date:**
This CLA is effective as of the date of the first Contribution made by the Contributor to the Papermark project.
This CLA is effective as of the date of the first Contribution made by the Contributor to the Hanzo Dataroom project.
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+53
View File
@@ -0,0 +1,53 @@
FROM ghcr.io/hanzoai/nodejs:v24.18.0 AS base
RUN apk add --no-cache libc6-compat openssl python3 make g++
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
COPY prisma ./prisma
ENV HUSKY=0
RUN npm install --legacy-peer-deps
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV DOCKER_OUTPUT=1
# Dummy env vars to prevent module-scope crashes during Next.js build
# (OpenAI, Hanko, etc. initialize clients at import time)
ENV OPENAI_API_KEY=build-placeholder
ENV HANKO_API_KEY=build-placeholder
ENV NEXT_PUBLIC_HANKO_TENANT_ID=build-placeholder
RUN npx prisma generate
# SQLite has no Prisma enums; re-inject the enum objects the app imports from
# @prisma/client (e.g. LinkType.DOCUMENT_LINK) so runtime/prerender resolves them.
RUN node prisma/inject-sqlite-enums.cjs
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm run build
FROM ghcr.io/hanzoai/nodejs:v24.18.0 AS runner
RUN apk add --no-cache openssl
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
RUN ln -s /app/prisma/migrations /app/prisma/schema/migrations
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.bin ./node_modules/.bin
USER nextjs
EXPOSE 3000
ENV PORT=3000
# SQLite: reconcile schema on startup via `db push` (Prisma has no migrate-deploy
# path for the sqlite provider here; the pg migration history under prisma/migrations
# is postgres-specific and unused). Schema folder = prismaSchemaFolder preview.
CMD ["sh", "-c", "node_modules/.bin/prisma db push --schema prisma/schema --skip-generate --accept-data-loss && node server.js"]
+49
View File
@@ -0,0 +1,49 @@
# Hanzo Dataroom
## Overview
Hanzo dataroom service.
**Upstream**: [Papermark](https://github.com/mfts/papermark) (AGPL-3.0). LICENSE retains "Copyright (c) 2023-present Papermark, Inc." Open-source DocSend alternative. This fork is Hanzo Dataroom — single-license AGPL, no `ee/` commercial directory.
## In-process fold (HIP-0106, task #101)
The production runtime is the **goja bundle** in [`goja/`](goja/): the ESM-free
port of the API handlers, run in-process inside the unified `hanzoai/cloud` binary
over Hanzo Base/SQLite (per-tenant) + the cloud object-storage seam. The standalone
Next.js app + Postgres pod is **retired**; cloud serves `/v1/dataroom/*` itself.
See [`goja/README.md`](goja/README.md) for the host contract. The Next.js/TS
sources below remain the reference for the domain model that `goja/bundle.js`
implements.
## Tech Stack
- **Language**: TypeScript/JavaScript
## Build & Run
```bash
npm install && npm run build
npm test
```
## Structure
```
dataroom/
CLA.md
Dockerfile
LICENSE
LLM.md
Pipfile
Pipfile.lock
README.md
SECURITY.md
app/
components/
components.json
context/
features/
lib/
middleware.ts
```
## Key Files
- `README.md` -- Project documentation
- `package.json` -- Dependencies and scripts
- `Dockerfile` -- Container build
+17 -15
View File
@@ -1,26 +1,28 @@
<p align="center"><img src=".github/hero.svg" alt="dataroom" width="880"></p>
<div align="center">
<h1 align="center">Papermark</h1>
<h1 align="center">Hanzo Dataroom</h1>
<h3>The open-source DocSend alternative.</h3>
<a target="_blank" href="https://www.producthunt.com/posts/papermark-3?utm_source=badge-top-post-badge&amp;utm_medium=badge&amp;utm_souce=badge-papermark"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=411605&amp;theme=light&amp;period=daily" alt="Papermark - The open-source DocSend alternative | Product Hunt" style="width:250px;height:40px"></a>
<a target="_blank" href="https://www.producthunt.com/posts/hanzo-dataroom-3?utm_source=badge-top-post-badge&amp;utm_medium=badge&amp;utm_souce=badge-hanzo-dataroom"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=411605&amp;theme=light&amp;period=daily" alt="Hanzo Dataroom - The open-source DocSend alternative | Product Hunt" style="width:250px;height:40px"></a>
</div>
<div align="center">
<a href="https://www.papermark.com">papermark.com</a>
<a href="https://www.dataroom.hanzo.ai">dataroom.hanzo.ai</a>
</div>
<br/>
<div align="center">
<a href="https://github.com/mfts/papermark/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/mfts/papermark"></a>
<a href="https://twitter.com/papermarkio"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/papermarkio"></a>
<a href="https://github.com/mfts/papermark/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-purple"></a>
<a href="https://github.com/hanzoai/dataroom/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/hanzoai/dataroom"></a>
<a href="https://twitter.com/hanzoai"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/hanzoai"></a>
<a href="https://github.com/hanzoai/dataroom/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-purple"></a>
</div>
<br/>
Papermark is the open-source document-sharing alternative to DocSend, featuring built-in analytics and custom domains.
Hanzo Dataroom is the open-source document-sharing alternative to DocSend, featuring built-in analytics and custom domains.
## Features
@@ -31,7 +33,7 @@ Papermark is the open-source document-sharing alternative to DocSend, featuring
## Demo
![Papermark Welcome GIF](.github/images/papermark-welcome.gif)
![Hanzo Dataroom Welcome GIF](.github/images/hanzo-dataroom-welcome.gif)
## Tech Stack
@@ -51,7 +53,7 @@ Papermark is the open-source document-sharing alternative to DocSend, featuring
### Prerequisites
Here's what you need to run Papermark:
Here's what you need to run Hanzo Dataroom:
- Node.js (version >= 18.17.0)
- PostgreSQL Database
@@ -61,8 +63,8 @@ Here's what you need to run Papermark:
### 1. Clone the repository
```shell
git clone https://github.com/mfts/papermark.git
cd papermark
git clone https://github.com/hanzoai/dataroom.git
cd hanzo-dataroom
```
### 2. Install npm dependencies
@@ -119,19 +121,19 @@ To prepare the Tinybird database, follow these steps:
pipenv shell
## start: pkgx-specific
cd ..
cd papermark
cd hanzo-dataroom
## end: pkgx-specific
pipenv update tinybird-cli
```
## Contributing
Papermark is an open-source project, and we welcome contributions from the community.
Hanzo Dataroom is an open-source project, and we welcome contributions from the community.
If you'd like to contribute, please fork the repository and make any changes you'd like. Pull requests are warmly welcome.
### Our Contributors ✨
<a href="https://github.com/mfts/papermark/graphs/contributors">
<img src="https://contrib.rocks/image?repo=mfts/papermark" />
<a href="https://github.com/hanzoai/dataroom/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hanzoai/dataroom" />
</a>
+2 -2
View File
@@ -2,10 +2,10 @@
## Supported Versions
The latest version of Papermark is currently being supported with security updates.
The latest version of Hanzo Dataroom is currently being supported with security updates.
## Reporting a Vulnerability
To report a vulnerability, send an email to security@papermark.com.
To report a vulnerability, send an email to security@dataroom.hanzo.ai.
We will respond within 48 hours acknowledging your report with details about next steps and potential rewards/compensation for responsible disclosure.
@@ -20,19 +20,19 @@ export const runtime = "nodejs";
const data = {
description: "Confirm email change",
title: "Confirm email change | Papermark",
title: "Confirm email change | Hanzo Dataroom",
url: "/auth/confirm-email-change",
};
export const metadata: Metadata = {
metadataBase: new URL("https://www.papermark.com"),
metadataBase: new URL("https://dataroom.hanzo.ai"),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Papermark",
siteName: "Hanzo Dataroom",
images: [
{
url: "/_static/meta-image.png",
@@ -47,13 +47,13 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@papermarkio",
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
interface PageProps {
params: { token: string };
params: Promise<{ token: string }>;
}
export default async function ConfirmEmailChangePage(props: PageProps) {
@@ -64,7 +64,8 @@ export default async function ConfirmEmailChangePage(props: PageProps) {
);
}
const VerifyEmailChange = async ({ params: { token } }: PageProps) => {
const VerifyEmailChange = async ({ params }: PageProps) => {
const { token } = await params;
const tokenFound = await prisma.verificationToken.findUnique({
where: {
token: hashToken(token),
@@ -115,10 +115,10 @@ export default function EmailVerificationClient() {
<div className="flex w-full justify-center bg-gray-50 md:w-1/2 lg:w-1/2">
<div className="z-10 mx-5 mt-[calc(1vh)] h-fit w-full max-w-md overflow-hidden rounded-lg sm:mx-0 sm:mt-[calc(2vh)] md:mt-[calc(3vh)]">
<div className="items-left flex flex-col space-y-3 px-4 py-6 pt-8 sm:px-12">
<Link href="https://www.papermark.com" target="_blank">
<Link href="https://dataroom.hanzo.ai" target="_blank">
<img
src="/_static/papermark-logo.svg"
alt="Papermark Logo"
src="/_static/hanzo-dataroom-logo.svg"
alt="Hanzo Dataroom Logo"
className="-mt-8 mb-36 h-7 w-auto self-start sm:mb-32 md:mb-48"
/>
</Link>
@@ -153,10 +153,10 @@ export default function EmailVerificationClient() {
></div>
<div className="z-10 mx-5 mt-[calc(1vh)] h-fit w-full max-w-md overflow-hidden rounded-lg sm:mx-0 sm:mt-[calc(2vh)] md:mt-[calc(3vh)]">
<div className="items-left flex flex-col space-y-3 px-4 py-6 pt-8 sm:px-12">
<Link href="https://www.papermark.com" target="_blank">
<Link href="https://dataroom.hanzo.ai" target="_blank">
<img
src="/_static/papermark-logo.svg"
alt="Papermark Logo"
src="/_static/hanzo-dataroom-logo.svg"
alt="Hanzo Dataroom Logo"
className="-mt-8 mb-36 h-7 w-auto self-start sm:mb-32 md:mb-48"
/>
</Link>
@@ -187,10 +187,10 @@ export default function EmailVerificationClient() {
<p className="mt-2 text-sm text-orange-800">
Check your junk/spam and quarantine folders and ensure that{" "}
<a
href="mailto:system@papermark.com"
href="mailto:dataroom@hanzo.ai"
className="font-medium text-orange-600 underline hover:text-orange-700"
>
system@papermark.com
dataroom@hanzo.ai
</a>{" "}
is on your allowed senders list.
</p>
@@ -262,7 +262,7 @@ export default function EmailVerificationClient() {
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-12">
By clicking continue, you acknowledge that you have read and agree
to Papermark&apos;s{" "}
to Hanzo Dataroom&apos;s{" "}
<a
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/terms`}
target="_blank"
@@ -312,7 +312,7 @@ function TestimonialSection() {
<div className="max-w-xl text-center">
<blockquote className="text-balance font-normal leading-8 text-white sm:text-xl sm:leading-9">
<p>
&quot;We raised our 30M Fund with Papermark Data Rooms. Love
&quot;We raised our 30M Fund with Hanzo Dataroom. Love
the customization, security and ease of use.&quot;
</p>
</blockquote>
+5 -5
View File
@@ -3,20 +3,20 @@ import { Metadata } from "next";
import EmailVerificationClient from "./page-client";
const data = {
description: "Verify your login to Papermark",
title: "Verify Login | Papermark",
description: "Verify your login to Hanzo Dataroom",
title: "Verify Login | Hanzo Dataroom",
url: "/auth/email",
};
export const metadata: Metadata = {
metadataBase: new URL("https://www.papermark.com"),
metadataBase: new URL("https://dataroom.hanzo.ai"),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Papermark",
siteName: "Hanzo Dataroom",
images: [
{
url: "/_static/meta-image.png",
@@ -31,7 +31,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@papermarkio",
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
+7 -2
View File
@@ -1,12 +1,17 @@
import { Metadata } from "next";
import { Suspense } from "react";
import SAMLCallbackClient from "./page-client";
export const metadata: Metadata = {
title: "SSO Login | Papermark",
title: "SSO Login | Hanzo Dataroom",
description: "Completing SSO login",
};
export default function SAMLCallbackPage() {
return <SAMLCallbackClient />;
return (
<Suspense>
<SAMLCallbackClient />
</Suspense>
);
}
+74 -300
View File
@@ -1,318 +1,92 @@
"use client";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useParams } from "next/navigation";
import { useState } from "react";
import { AlertCircle } from "lucide-react";
import { SSOLogin } from "@/ee/features/security/sso";
import { signInWithPasskey } from "@teamhanko/passkeys-next-auth-provider/client";
import { signIn } from "next-auth/react";
import { toast } from "sonner";
import { z } from "zod";
import { cn } from "@/lib/utils";
import { LastUsed, useLastUsed } from "@/components/hooks/useLastUsed";
import Google from "@/components/shared/icons/google";
import LinkedIn from "@/components/shared/icons/linkedin";
import Passkey from "@/components/shared/icons/passkey";
import { LogoCloud } from "@/components/shared/logo-cloud";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { HanzoMark } from "@/components/shared/icons/hanzo-mark";
// White-label hooks: every visible brand token comes from env so tenants
// drop in their own name/words/IAM provider without touching this file.
//
// NEXT_PUBLIC_APP_NAME e.g. "Hanzo Dataroom" | "Acme Rooms"
// NEXT_PUBLIC_APP_NAME_PRIMARY first word of wordmark; defaults to first
// word of NEXT_PUBLIC_APP_NAME ("Hanzo")
// NEXT_PUBLIC_APP_NAME_SUFFIX second token of wordmark; defaults to
// remainder of NEXT_PUBLIC_APP_NAME ("Dataroom")
// NEXT_PUBLIC_APP_TAGLINE one-line under the welcome headline
// NEXT_PUBLIC_IAM_PROVIDER_NAME IAM brand for the "Sign in with X" button
// NEXT_PUBLIC_MARKETING_URL base URL the Terms / Privacy links resolve against
const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "Hanzo Dataroom";
const [defaultPrimary, ...defaultSuffixParts] = APP_NAME.split(" ");
const APP_NAME_PRIMARY =
process.env.NEXT_PUBLIC_APP_NAME_PRIMARY || defaultPrimary || APP_NAME;
const APP_NAME_SUFFIX =
process.env.NEXT_PUBLIC_APP_NAME_SUFFIX || defaultSuffixParts.join(" ");
const APP_TAGLINE =
process.env.NEXT_PUBLIC_APP_TAGLINE || "Share documents. Not attachments.";
const IAM_PROVIDER_NAME =
process.env.NEXT_PUBLIC_IAM_PROVIDER_NAME || "Hanzo";
const MARKETING_URL = process.env.NEXT_PUBLIC_MARKETING_URL || "";
export default function Login() {
const { next } = useParams as { next?: string };
const router = useRouter();
const searchParams = useSearchParams();
const authError = searchParams?.get("error");
const isSSORequired = authError === "require-saml-sso";
const [lastUsed, setLastUsed] = useLastUsed();
const authMethods = ["google", "email", "linkedin", "passkey"] as const;
type AuthMethod = (typeof authMethods)[number];
const [clickedMethod, setClickedMethod] = useState<AuthMethod | undefined>(
undefined,
);
const [email, setEmail] = useState<string>("");
const [emailButtonText, setEmailButtonText] = useState<string>(
"Continue with Email",
);
const emailSchema = z
.string()
.trim()
.toLowerCase()
.min(3, { message: "Please enter a valid email." })
.email({ message: "Please enter a valid email." });
const emailValidation = emailSchema.safeParse(email);
return (
<div className="flex h-screen w-full flex-wrap">
{/* Left part */}
<div className="flex w-full justify-center bg-gray-50 md:w-1/2 lg:w-1/2">
<div
className="absolute inset-x-0 top-10 -z-10 flex transform-gpu justify-center overflow-hidden blur-3xl"
aria-hidden="true"
></div>
<div className="z-10 mx-5 mt-[calc(1vh)] h-fit w-full max-w-md overflow-hidden rounded-lg sm:mx-0 sm:mt-[calc(2vh)] md:mt-[calc(3vh)]">
<div className="items-left flex flex-col space-y-3 px-4 py-6 pt-8 sm:px-12">
<Link href="https://www.papermark.com" target="_blank">
<img
src="/_static/papermark-logo.svg"
alt="Papermark Logo"
className="md:mb-48s -mt-8 mb-36 h-7 w-auto self-start sm:mb-32"
/>
</Link>
<Link href="/">
<span className="text-balance text-3xl font-semibold text-gray-900">
Welcome to Papermark
</span>
</Link>
<h3 className="text-balance text-sm text-gray-800">
Share documents. Not attachments.
</h3>
</div>
{isSSORequired && (
<div className="mx-4 mb-2 flex items-start gap-3 rounded-lg border border-orange-200 bg-orange-50 px-4 py-3 sm:mx-12">
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-orange-600" />
<div>
<p className="text-sm font-medium text-orange-900">
Your organization requires SSO login
</p>
<p className="mt-1 text-sm text-orange-700">
Please use the <strong>SAML SSO</strong> option below to sign
in with your company&apos;s identity provider.
</p>
</div>
</div>
)}
<form
className="flex flex-col gap-4 px-4 pt-8 sm:px-12"
onSubmit={(e) => {
e.preventDefault();
if (!emailValidation.success) {
toast.error(emailValidation.error.errors[0].message);
return;
}
<main className="flex min-h-screen w-full items-center justify-center bg-black px-6 text-white">
<div className="w-full max-w-sm">
<Link href="/" className="mb-12 flex items-center gap-3">
<HanzoMark size={28} className="text-white" />
<span className="text-base font-medium tracking-tight">
{APP_NAME_PRIMARY}
{APP_NAME_SUFFIX && (
<span className="text-zinc-400"> {APP_NAME_SUFFIX}</span>
)}
</span>
</Link>
setClickedMethod("email");
signIn("email", {
email: emailValidation.data,
redirect: false,
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
if (res?.ok && !res?.error) {
setLastUsed("credentials");
// Store email in sessionStorage for the verification page
try {
sessionStorage.setItem(
"pendingVerificationEmail",
emailValidation.data,
);
} catch {
// sessionStorage not available, verification page will show email input
}
router.push("/auth/email");
} else {
setEmailButtonText("Error sending email - try again?");
toast.error("Error sending email - try again?");
setClickedMethod(undefined);
}
});
}}
<h1 className="text-balance text-3xl font-semibold text-white">
Welcome to {APP_NAME}
</h1>
<p className="mt-2 text-balance text-sm text-zinc-400">{APP_TAGLINE}</p>
<Button
onClick={() =>
signIn("hanzo-iam", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
})
}
className="mt-8 flex w-full items-center justify-center bg-white font-normal text-black hover:bg-zinc-200"
>
<span>
Sign in with <span className="font-bold">{IAM_PROVIDER_NAME}</span>
</span>
</Button>
<p className="mt-8 text-xs text-zinc-500">
By clicking continue, you acknowledge that you have read and agree to{" "}
{APP_NAME}&apos;s{" "}
<a
href={`${MARKETING_URL}/terms`}
target="_blank"
className="text-zinc-300 underline hover:text-white"
>
<Label className="sr-only" htmlFor="email">
Email
</Label>
<Input
id="email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={clickedMethod === "email"}
// pattern={patternSimpleEmailRegex}
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
"flex h-10 w-full rounded-md border-0 bg-background bg-white px-3 py-2 text-sm text-gray-900 ring-1 ring-gray-200 transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white",
email.length > 0 && !emailValidation.success
? "ring-red-500"
: "ring-gray-200",
)}
/>
<div className="relative">
<Button
type="submit"
loading={clickedMethod === "email"}
disabled={!emailValidation.success || !!clickedMethod}
className={cn(
"focus:shadow-outline w-full transform rounded px-4 py-2 text-white transition-colors duration-300 ease-in-out focus:outline-none",
clickedMethod === "email"
? "bg-black"
: "bg-gray-800 hover:bg-gray-900",
)}
>
{emailButtonText}
</Button>
{lastUsed === "credentials" && <LastUsed />}
</div>
</form>
<p className="py-4 text-center">or</p>
<div className="flex flex-col space-y-2 px-4 sm:px-12">
<div className="relative">
<Button
onClick={() => {
setClickedMethod("google");
setLastUsed("google");
signIn("google", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
setClickedMethod(undefined);
});
}}
loading={clickedMethod === "google"}
disabled={clickedMethod && clickedMethod !== "google"}
className="flex w-full items-center justify-center space-x-2 border border-gray-300 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
>
<Google className="h-5 w-5" />
<span>Continue with Google</span>
{clickedMethod !== "google" && lastUsed === "google" && (
<LastUsed />
)}
</Button>
</div>
<div className="relative">
<Button
onClick={() => {
setClickedMethod("linkedin");
setLastUsed("linkedin");
signIn("linkedin", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
}).then((res) => {
setClickedMethod(undefined);
});
}}
loading={clickedMethod === "linkedin"}
disabled={clickedMethod && clickedMethod !== "linkedin"}
className="flex w-full items-center justify-center space-x-2 border border-gray-300 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200"
>
<LinkedIn />
<span>Continue with LinkedIn</span>
{clickedMethod !== "linkedin" && lastUsed === "linkedin" && (
<LastUsed />
)}
</Button>
</div>
<div className="relative">
<Button
onClick={() => {
setLastUsed("passkey");
setClickedMethod("passkey");
signInWithPasskey({
tenantId: process.env.NEXT_PUBLIC_HANKO_TENANT_ID as string,
}).then(() => {
setClickedMethod(undefined);
});
}}
variant="outline"
loading={clickedMethod === "passkey"}
disabled={clickedMethod && clickedMethod !== "passkey"}
className="flex w-full items-center justify-center space-x-2 border border-gray-300 bg-gray-100 font-normal text-gray-900 hover:bg-gray-200 hover:text-gray-900"
>
<Passkey className="h-4 w-4" />
<span>Continue with a passkey</span>
{lastUsed === "passkey" && <LastUsed />}
</Button>
</div>
<div className="relative">
<SSOLogin autoExpand={isSSORequired} />
</div>
</div>
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-12">
By clicking continue, you acknowledge that you have read and agree
to Papermark&apos;s{" "}
<a
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/terms`}
target="_blank"
className="underline"
>
Terms of Service
</a>{" "}
and{" "}
<a
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/privacy`}
target="_blank"
className="underline"
>
Privacy Policy
</a>
.
</p>
</div>
</div>
<div className="relative hidden w-full justify-center overflow-hidden bg-black md:flex md:w-1/2 lg:w-1/2">
<div className="relative m-0 flex h-full min-h-[700px] w-full p-0">
<div
className="relative flex h-full w-full flex-col justify-between"
id="features"
Terms of Service
</a>{" "}
and{" "}
<a
href={`${MARKETING_URL}/privacy`}
target="_blank"
className="text-zinc-300 underline hover:text-white"
>
{/* Testimonial top 2/3 */}
<div
className="flex w-full flex-col items-center justify-center"
style={{ height: "66.6666%" }}
>
{/* Image container */}
<div className="mb-4 h-64 w-80">
<img
className="h-full w-full rounded-2xl object-cover shadow-2xl"
src="/_static/testimonials/backtrace.jpeg"
alt="Backtrace Capital"
/>
</div>
{/* Text content */}
<div className="max-w-xl text-center">
<blockquote className="text-balance font-normal leading-8 text-white sm:text-xl sm:leading-9">
<p>
&quot;We raised our 30M Fund with Papermark Data Rooms.
Love the customization, security and ease of use.&quot;
</p>
</blockquote>
<figcaption className="mt-4">
<div className="text-balance font-normal text-white">
Michael Münnix
</div>
<div className="text-balance font-light text-gray-400">
Partner, Backtrace Capital
</div>
</figcaption>
</div>
</div>
{/* White block with logos bottom 1/3, full width/height */}
<div
className="absolute bottom-0 left-0 flex w-full flex-col items-center justify-center bg-white"
style={{ height: "33.3333%" }}
>
<div className="mb-4 max-w-xl text-balance text-center font-semibold text-gray-900">
Trusted by teams at
</div>
<LogoCloud />
{/* <img
src="https://assets.papermark.io/upload/file_7JEGY7zM9ZTfmxu8pe7vWj-Screenshot-2025-05-09-at-18.09.13.png"
alt="Trusted teams illustration"
className="mt-4 max-w-full h-auto object-contain"
style={{maxHeight: '120px'}}
/> */}
</div>
</div>
</div>
Privacy Policy
</a>
.
</p>
</div>
</div>
</main>
);
}
+10 -6
View File
@@ -1,24 +1,26 @@
import { Metadata } from "next";
import { Suspense } from "react";
import { APP_NAME, APP_URL } from "@/lib/branding";
import { GTMComponent } from "@/components/gtm-component";
import LoginClient from "./page-client";
const data = {
description: "Login to Papermark",
title: "Login | Papermark",
description: `Login to ${APP_NAME}`,
title: `Login | ${APP_NAME}`,
url: "/login",
};
export const metadata: Metadata = {
metadataBase: new URL("https://www.papermark.com"),
metadataBase: new URL(APP_URL),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Papermark",
siteName: APP_NAME,
images: [
{
url: "/_static/meta-image.png",
@@ -33,7 +35,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@papermarkio",
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
@@ -42,7 +44,9 @@ export default function LoginPage() {
return (
<>
<GTMComponent />
<LoginClient />
<Suspense>
<LoginClient />
</Suspense>
</>
);
}
+4 -4
View File
@@ -6,7 +6,7 @@ import { useParams } from "next/navigation";
import { useState } from "react";
import PapermarkLogo from "@/public/_static/papermark-logo.svg";
import HanzoLogo from "@/public/_static/hanzo-dataroom-logo.svg";
import { signIn } from "next-auth/react";
import { toast } from "sonner";
@@ -35,12 +35,12 @@ export default function Register() {
</div>
<div className="z-10 mx-5 mt-[calc(20vh)] h-fit w-full max-w-md overflow-hidden rounded-lg border border-border bg-gray-50 dark:bg-gray-900 sm:mx-0 sm:shadow-xl">
<div className="flex flex-col items-center justify-center space-y-3 px-4 py-6 pt-8 text-center sm:px-16">
<Link href="https://www.papermark.com" target="_blank">
<Link href="https://dataroom.hanzo.ai" target="_blank">
<Image
src={PapermarkLogo}
src={HanzoLogo}
width={119}
height={32}
alt="Papermark Logo"
alt="Hanzo Dataroom Logo"
/>
</Link>
<h3 className="text-2xl font-medium text-foreground">
+6 -5
View File
@@ -1,22 +1,23 @@
import { Metadata } from "next";
import { APP_NAME, APP_URL } from "@/lib/branding";
import RegisterClient from "./page-client";
const data = {
description: "Signup to Papermark",
title: "Sign up | Papermark",
description: `Signup to ${APP_NAME}`,
title: `Sign up | ${APP_NAME}`,
url: "/register",
};
export const metadata: Metadata = {
metadataBase: new URL("https://www.papermark.com"),
metadataBase: new URL(APP_URL),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Papermark",
siteName: APP_NAME,
images: [
{
url: "/_static/meta-image.png",
@@ -31,7 +32,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@papermarkio",
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
@@ -37,7 +37,7 @@ export default function InvitationStatusContent({
</div>
<div className="w-full space-y-4">
<h4 className="text-center text-sm font-medium text-gray-800">
Create your own Papermark account
Create your own Hanzo Dataroom account
</h4>
<div className="space-y-3">
<Link href="/login" className="block w-full">
+12 -12
View File
@@ -13,20 +13,20 @@ import InvitationStatusContent from "./InvitationStatusContent";
import CleanUrlOnExpire from "./status/ClientRedirect";
const data = {
description: "Accept your team invitation on Papermark",
title: "Accept Invitation | Papermark",
description: "Accept your team invitation on Hanzo Dataroom",
title: "Accept Invitation | Hanzo Dataroom",
url: "/verify/invitation",
};
export const metadata: Metadata = {
metadataBase: new URL("https://www.papermark.com"),
metadataBase: new URL("https://dataroom.hanzo.ai"),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Papermark",
siteName: "Hanzo Dataroom",
images: [
{
url: "/_static/meta-image.png",
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@papermarkio",
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
@@ -49,11 +49,11 @@ export const metadata: Metadata = {
export default async function VerifyInvitationPage({
searchParams,
}: {
searchParams: {
searchParams: Promise<{
token?: string;
};
}>;
}) {
const { token: jwtToken } = searchParams;
const { token: jwtToken } = await searchParams;
if (!jwtToken) {
return <NotFound />;
@@ -100,13 +100,13 @@ export default async function VerifyInvitationPage({
<div className="flex flex-col items-center justify-center space-y-3 px-4 py-6 pt-8 text-center sm:px-16">
<Link href="/">
<span className="text-balance text-2xl font-semibold text-gray-800">
Welcome to Papermark
Welcome to Hanzo Dataroom
</span>
</Link>
{!isExpired && !isRevoked && (
<>
<h3 className="text-balance py-1 text-sm font-normal text-gray-800">
You&apos;ve been invited to join a team on Papermark
You&apos;ve been invited to join a team on Hanzo Dataroom
</h3>
<div className="mt-2 flex w-auto items-center justify-center gap-2 rounded-full bg-gray-50 px-5 py-2.5 text-sm text-gray-600 shadow-sm">
<MailIcon className="h-4 w-4 text-gray-400" />
@@ -148,7 +148,7 @@ export default async function VerifyInvitationPage({
</div>
<p className="mt-10 w-full max-w-md px-4 text-xs text-muted-foreground sm:px-16">
By accepting this invitation, you acknowledge that you have
read and agree to Papermark&apos;s{" "}
read and agree to Hanzo Dataroom&apos;s{" "}
<a
href={`${process.env.NEXT_PUBLIC_MARKETING_URL}/terms`}
target="_blank"
@@ -191,7 +191,7 @@ export default async function VerifyInvitationPage({
<blockquote className="text-l text-balance leading-8 text-gray-100 sm:text-xl sm:leading-9">
<p>
True builders listen to their users and build what they
need. Thanks Papermark team for solving a big pain point.
need. Thanks Hanzo Dataroom team for solving a big pain point.
DocSend monopoly will end soon!
</p>
</blockquote>
-1
View File
@@ -1 +0,0 @@
../ee/LICENSE.md
-1
View File
@@ -1 +0,0 @@
../ee/README.md
@@ -1,10 +1,10 @@
import { NextRequest } from "next/server";
import { generateChatTitle } from "@/ee/features/ai/lib/chat/generate-chat-title";
import { getFilteredDataroomDocumentIds } from "@/ee/features/ai/lib/chat/get-filtered-dataroom-document-ids";
import { sendMessage } from "@/ee/features/ai/lib/chat/send-message";
import { validateChatAccess } from "@/ee/features/ai/lib/permissions/validate-chat-access";
import { sendMessageSchema } from "@/ee/features/ai/schemas/chat";
import { generateChatTitle } from "@/features/ai/lib/chat/generate-chat-title";
import { getFilteredDataroomDocumentIds } from "@/features/ai/lib/chat/get-filtered-dataroom-document-ids";
import { sendMessage } from "@/features/ai/lib/chat/send-message";
import { validateChatAccess } from "@/features/ai/lib/permissions/validate-chat-access";
import { sendMessageSchema } from "@/features/ai/schemas/chat";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
@@ -35,8 +35,11 @@ export async function POST(
);
}
const { content, filterDocumentId, filterDataroomDocumentIds } =
validation.data;
const {
content,
filterDocumentId,
filterDataroomDocumentIds,
} = validation.data;
const session = await getServerSession(authOptions);
const searchParams = req.nextUrl.searchParams;
@@ -133,16 +136,49 @@ export async function POST(
}
// Send message and get streaming response
const result = await sendMessage({
const { result, referencesForStream } = await sendMessage({
chatId,
content,
vectorStoreId: chat.vectorStoreId,
filteredDataroomDocumentIds,
filterDocumentId,
userSelectedDataroomDocumentIds: filterDataroomDocumentIds,
dataroomId: chat.dataroomId || undefined,
linkId: chat.linkId || undefined,
});
return result.toTextStreamResponse();
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
try {
for await (const chunk of result.textStream) {
controller.enqueue(encoder.encode(chunk));
}
const referencesSection = await Promise.race([
referencesForStream,
new Promise<string>((resolve) =>
setTimeout(() => resolve(""), 5000),
),
]);
if (referencesSection) {
controller.enqueue(encoder.encode(referencesSection));
}
controller.close();
} catch (error) {
console.error("Error streaming AI response:", error);
controller.error(error);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
});
} catch (error) {
console.error("Error sending message:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { validateChatAccess } from "@/ee/features/ai/lib/permissions/validate-chat-access";
import { validateChatAccess } from "@/features/ai/lib/permissions/validate-chat-access";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { createChat } from "@/ee/features/ai/lib/chat/create-chat";
import { createChatSchema } from "@/ee/features/ai/schemas/chat";
import { createChat } from "@/features/ai/lib/chat/create-chat";
import { createChatSchema } from "@/features/ai/schemas/chat";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
import { z } from "zod";
@@ -4,8 +4,8 @@ import {
SUPPORTED_AI_CONTENT_TYPES,
addFileToVectorStoreTask,
processDocumentForAITask,
} from "@/ee/features/ai/lib/trigger";
import { createDataroomVectorStore } from "@/ee/features/ai/lib/vector-stores/create-dataroom-vector-store";
} from "@/features/ai/lib/trigger";
import { createDataroomVectorStore } from "@/features/ai/lib/vector-stores/create-dataroom-vector-store";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
@@ -4,9 +4,9 @@ import {
addFileToVectorStoreTask,
processDocumentForAITask,
SUPPORTED_AI_CONTENT_TYPES,
} from "@/ee/features/ai/lib/trigger";
import { createTeamVectorStore } from "@/ee/features/ai/lib/vector-stores/create-team-vector-store";
import { removeFileFromVectorStore } from "@/ee/features/ai/lib/vector-stores/remove-file-from-vector-store";
} from "@/features/ai/lib/trigger";
import { createTeamVectorStore } from "@/features/ai/lib/vector-stores/create-team-vector-store";
import { removeFileFromVectorStore } from "@/features/ai/lib/vector-stores/remove-file-from-vector-store";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getVectorStoreInfo } from "@/ee/features/ai/lib/vector-stores/get-vector-store-info";
import { getVectorStoreInfo } from "@/features/ai/lib/vector-stores/get-vector-store-info";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { receiver } from "@/lib/cron";
import { processDataroomDigest } from "@/lib/emails/process-dataroom-digest";
import { log } from "@/lib/utils";
// Runs daily at 9 AM UTC (0 9 * * *)
export const maxDuration = 300;
export async function POST(req: Request) {
const body = await req.json();
if (process.env.VERCEL === "1") {
const isValid = await receiver.verify({
signature: req.headers.get("Upstash-Signature") || "",
body: JSON.stringify(body),
});
if (!isValid) {
return new Response("Unauthorized", { status: 401 });
}
}
try {
const result = await processDataroomDigest("daily");
return NextResponse.json({ success: true, ...result });
} catch (error) {
await log({
message: `Daily dataroom digest cron failed. \n\nError: ${(error as Error).message}`,
type: "cron",
mention: true,
});
return NextResponse.json({ error: (error as Error).message });
}
}
@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { receiver } from "@/lib/cron";
import { processDataroomDigest } from "@/lib/emails/process-dataroom-digest";
import { log } from "@/lib/utils";
// Runs weekly on Monday at 9 AM UTC (0 9 * * 1)
export const maxDuration = 300;
export async function POST(req: Request) {
const body = await req.json();
if (process.env.VERCEL === "1") {
const isValid = await receiver.verify({
signature: req.headers.get("Upstash-Signature") || "",
body: JSON.stringify(body),
});
if (!isValid) {
return new Response("Unauthorized", { status: 401 });
}
}
try {
const result = await processDataroomDigest("weekly");
return NextResponse.json({ success: true, ...result });
} catch (error) {
await log({
message: `Weekly dataroom digest cron failed. \n\nError: ${(error as Error).message}`,
type: "cron",
mention: true,
});
return NextResponse.json({ error: (error as Error).message });
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ export async function POST(req: Request) {
where: {
slug: {
not: {
in: ["papermark.io", "papermark.com"],
in: ["dataroom.hanzo.ai", "dataroom.hanzo.ai"],
},
},
},
@@ -10,6 +10,107 @@ import { supportsAdvancedExcelMode } from "@/lib/utils/get-content-type";
import { runs } from "@trigger.dev/sdk/v3";
import { waitUntil } from "@vercel/functions";
/**
* GET /api/links/[id]/upload?dataroomId=xxx
* Returns the viewer's previously uploaded documents for this dataroom.
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } },
) {
try {
const linkId = params.id;
const dataroomId = request.nextUrl.searchParams.get("dataroomId");
if (!linkId || !dataroomId) {
return NextResponse.json(
{ message: "Missing required parameters" },
{ status: 400 },
);
}
// Verify the dataroom session
const dataroomSession = await verifyDataroomSession(
request,
linkId,
dataroomId,
);
if (!dataroomSession || !dataroomSession.viewerId) {
return NextResponse.json(
{ message: "Unauthorized" },
{ status: 401 },
);
}
const { viewerId } = dataroomSession;
// Fetch the viewer's uploads for this dataroom
const uploads = await prisma.documentUpload.findMany({
where: {
viewerId,
dataroomId,
linkId,
},
select: {
id: true,
documentId: true,
dataroomDocumentId: true,
originalFilename: true,
uploadedAt: true,
document: {
select: {
id: true,
name: true,
type: true,
versions: {
where: { isPrimary: true },
select: {
id: true,
hasPages: true,
},
take: 1,
},
},
},
dataroomDocument: {
select: {
folderId: true,
},
},
},
orderBy: { uploadedAt: "desc" },
});
const formattedUploads = uploads.map((upload) => {
const fileType = upload.document?.type ?? "";
const hasPages = upload.document?.versions?.[0]?.hasPages ?? false;
const needsProcessing = ["pdf", "docs", "slides"].includes(fileType);
const isComplete = !needsProcessing || hasPages;
return {
id: upload.id,
documentId: upload.documentId,
dataroomDocumentId: upload.dataroomDocumentId,
documentVersionId: upload.document?.versions?.[0]?.id ?? null,
name: upload.originalFilename ?? upload.document?.name ?? "Unknown",
fileType,
folderId: upload.dataroomDocument?.folderId ?? null,
uploadedAt: upload.uploadedAt,
status: isComplete ? "complete" : "processing",
};
});
return NextResponse.json({ uploads: formattedUploads });
} catch (error) {
console.error("Error fetching viewer uploads:", error);
return NextResponse.json(
{ message: "Error fetching uploads" },
{ status: 500 },
);
}
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } },
@@ -167,7 +268,7 @@ export async function POST(
viewId: viewId,
linkId: linkId,
originalFilename: document.name,
fileSize: document.versions[0].fileSize,
fileSize: documentData.fileSize ?? 0,
numPages: document.numPages,
mimeType: document.contentType,
dataroomId: dataroomId,
@@ -226,7 +327,20 @@ export async function POST(
}
}
return NextResponse.json({ success: true });
// Return document data for optimistic UI rendering
return NextResponse.json({
success: true,
document: {
id: document.id,
name: document.name,
dataroomDocumentId: newDataroomDocument.id,
documentVersionId: document.versions[0]?.id,
folderId: dataroomFolderId,
fileType: document.type,
hasPages: (document.numPages ?? 0) > 0,
createdAt: document.createdAt,
},
});
} catch (error) {
console.error("Error uploading document:", error);
return NextResponse.json(
+2 -2
View File
@@ -10,7 +10,7 @@ export const runtime = "edge";
*/
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const title = searchParams.get("title") || "Papermark Document";
const title = searchParams.get("title") || "Hanzo Dataroom Document";
const Inter = await fetch(
new URL("@/public/_static/Inter-Bold.ttf", import.meta.url),
).then((res) => res.arrayBuffer());
@@ -18,7 +18,7 @@ export async function GET(request: NextRequest) {
return new ImageResponse(
(
<div tw="flex flex-col items-center justify-between w-full h-full bg-white text-black p-12">
<div tw="text-[32px] flex items-center tracking-tighter">Papermark</div>
<div tw="text-[32px] flex items-center tracking-tighter">Hanzo Dataroom</div>
<div tw="text-[42px] text-center">{title}</div>
<div tw="text-[32px] flex items-center">
Open-Source Document Sharing
+2 -2
View File
@@ -29,7 +29,7 @@ export async function GET(req: NextRequest) {
{/* Left Side Text */}
<div tw="flex flex-col text-white" style={{ marginLeft: "48px" }}>
<div tw="flex text-7xl font-bold mb-4 tracking-tighter">
Papermark
Hanzo Dataroom
</div>
<div tw="flex text-5xl mb-4">Year in Review</div>
<div tw="flex text-7xl font-bold">{year}</div>
@@ -49,7 +49,7 @@ export async function GET(req: NextRequest) {
{/* Header Section */}
<div tw="flex items-start p-8 items-center">
<div tw="flex text-2xl font-bold text-white tracking-tighter">
Papermark
Hanzo Dataroom
</div>
</div>
@@ -119,7 +119,7 @@ export async function POST(
const result = await directorySyncController.directories.create({
tenant: teamId,
product: jacksonProduct,
name: name || "Papermark SCIM Directory",
name: name || "Hanzo Dataroom SCIM Directory",
type: type || "azure-scim-v2",
});
+2 -2
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { reportDeniedAccessAttempt } from "@/ee/features/access-notifications";
import { getTeamStorageConfigById } from "@/ee/features/storage/config";
import { reportDeniedAccessAttempt } from "@/features/access-notifications";
import { getTeamStorageConfigById } from "@/features/storage/config";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { ItemType, LinkAudienceType } from "@prisma/client";
import { ipAddress, waitUntil } from "@vercel/functions";
+2 -2
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { reportDeniedAccessAttempt } from "@/ee/features/access-notifications";
import { getTeamStorageConfigById } from "@/ee/features/storage/config";
import { reportDeniedAccessAttempt } from "@/features/access-notifications";
import { getTeamStorageConfigById } from "@/features/storage/config";
// Import authOptions directly from the source
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { ipAddress, waitUntil } from "@vercel/functions";
@@ -1,11 +1,11 @@
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { WorkflowEngine } from "@/ee/features/workflows/lib/engine";
import { WorkflowEngine } from "@/features/workflows/lib/engine";
import {
AccessRequestSchema,
VerifyEmailRequestSchema,
} from "@/ee/features/workflows/lib/types";
} from "@/features/workflows/lib/types";
import { ipAddress, waitUntil } from "@vercel/functions";
import { z } from "zod";
@@ -1,8 +1,8 @@
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { WorkflowEngine } from "@/ee/features/workflows/lib/engine";
import { AccessRequestSchema } from "@/ee/features/workflows/lib/types";
import { WorkflowEngine } from "@/features/workflows/lib/engine";
import { AccessRequestSchema } from "@/features/workflows/lib/types";
import { ipAddress } from "@vercel/functions";
import { z } from "zod";
@@ -6,7 +6,7 @@ import { ratelimit } from "@/lib/redis";
import { sendOtpVerificationEmail } from "@/lib/emails/send-email-otp-verification";
import { generateOTP } from "@/lib/utils/generate-otp";
import { validateEmail } from "@/lib/utils/validate-email";
import { VerifyEmailRequestSchema } from "@/ee/features/workflows/lib/types";
import { VerifyEmailRequestSchema } from "@/features/workflows/lib/types";
// POST /app/(ee)/api/workflow-entry/[entryLinkId]/verify - Send OTP
export async function POST(
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
UpdateWorkflowRequestSchema,
formatZodError,
} from "@/ee/features/workflows/lib/validation";
} from "@/features/workflows/lib/validation";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { customAlphabet } from "nanoid";
import { getServerSession } from "next-auth";
@@ -5,7 +5,7 @@ import {
formatZodError,
validateActions,
validateConditions,
} from "@/ee/features/workflows/lib/validation";
} from "@/features/workflows/lib/validation";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
import { z } from "zod";
@@ -10,8 +10,8 @@ import {
formatZodError,
validateConditions,
validateActions,
} from "@/ee/features/workflows/lib/validation";
import { ReorderStepsRequest } from "@/ee/features/workflows/lib/types";
} from "@/features/workflows/lib/validation";
import { ReorderStepsRequest } from "@/features/workflows/lib/types";
// GET /app/(ee)/api/workflows/[workflowId]/steps?teamId=xxx - List all steps
export async function GET(
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
import {
CreateWorkflowRequestSchema,
formatZodError,
} from "@/ee/features/workflows/lib/validation";
} from "@/features/workflows/lib/validation";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
import { z } from "zod";
+8 -7
View File
@@ -5,22 +5,23 @@ import "@/styles/globals.css";
const inter = Inter({ subsets: ["latin"] });
import { APP_DESCRIPTION, APP_NAME, APP_URL } from "@/lib/branding";
const data = {
description:
"Papermark is an open-source document sharing infrastructure. Free alternative to Docsend with custom domain. Manage secure document sharing with real-time analytics.",
title: "Papermark | The Open Source DocSend Alternative",
description: APP_DESCRIPTION,
title: `${APP_NAME} | Secure Data Room Infrastructure`,
url: "/",
};
export const metadata: Metadata = {
metadataBase: new URL("https://www.papermark.com"),
metadataBase: new URL(APP_URL),
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
url: data.url,
siteName: "Papermark",
siteName: APP_NAME,
images: [
{
url: "/_static/meta-image.png",
@@ -35,7 +36,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: data.title,
description: data.description,
creator: "@papermarkio",
creator: "@hanzoai",
images: ["/_static/meta-image.png"],
},
};
@@ -46,7 +47,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en">
<html lang="en" className="dark">
<body className={inter.className}>{children}</body>
</html>
);
-1
View File
@@ -1,5 +1,4 @@
User-Agent: *
Disallow: /login
Disallow: /register
Disallow: /verify/
Disallow: /auth/
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
ColumnDef,
SortingState,
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
ColumnDef,
SortingState,
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { differenceInDays, format, startOfDay, subDays } from "date-fns";
import { CalendarIcon, ChevronDown, CrownIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
ColumnDef,
SortingState,
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
ColumnDef,
SortingState,
+2 -2
View File
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useTeam } from "@/context/team-context";
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
import { getQuantityFromPriceId } from "@/ee/stripe/functions/get-quantity-from-plan";
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
import { getQuantityFromPriceId } from "@/lib/billing/legacy/functions/get-quantity-from-plan";
import { toast } from "sonner";
import { useAnalytics } from "@/lib/analytics";
+16
View File
@@ -0,0 +1,16 @@
/**
* Stub `CancellationModal` the commercial cancellation funnel was removed
* with the AGPL paywall rip. We keep the component as a no-op so existing
* call sites compile.
*/
export type CancellationModalProps = {
open?: boolean;
onOpenChange?: (open: boolean) => void;
};
export function CancellationModal(_props: CancellationModalProps) {
return null;
}
export default CancellationModal;
+2 -2
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { Dispatch, SetStateAction, useState } from "react";
import { useTeam } from "@/context/team-context";
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
import Cookies from "js-cookie";
import { toast } from "sonner";
@@ -41,7 +41,7 @@ export default function ProAnnualBanner({
<span className="sr-only">Close</span>
</button>
<div className="flex space-x-2">
<span className="text-sm font-bold">Papermark Pro Annual </span>
<span className="text-sm font-bold">Hanzo Dataroom Pro Annual </span>
</div>
<p className="my-4 text-sm">
Lock in a better price and get 2 months free.
+2 -2
View File
@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction } from "react";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import Cookies from "js-cookie";
import X from "@/components/shared/icons/x";
@@ -31,7 +31,7 @@ export default function ProBanner({
<span className="sr-only">Close</span>
</button>
<div className="flex space-x-2">
<span className="text-sm font-bold"> Papermark Business </span>
<span className="text-sm font-bold"> Hanzo Dataroom Business </span>
</div>
<p className="my-4 text-sm">
Upgrade to unlock custom branding, team members, domains and data rooms.
@@ -3,8 +3,8 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { CancellationModal } from "@/ee/features/billing/cancellation/components";
import { PlanEnum } from "@/ee/stripe/constants";
import { CancellationModal } from "@/components/billing/cancellation-modal";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
BanIcon,
CirclePauseIcon,
@@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from "react";
import React from "react";
import { useTeam } from "@/context/team-context";
import { getStripe } from "@/ee/stripe/client";
import { PLANS } from "@/ee/stripe/utils";
import { getStripe } from "@/lib/billing/legacy/client";
import { PLANS } from "@/lib/billing/legacy/utils";
import { CheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -53,7 +53,7 @@ export function UpgradePlanModal({
"Custom branding",
"Folder organization",
"Require email verification",
"Papermark branding removed",
"Hanzo Dataroom branding removed",
"1-year analytics retention",
];
}
@@ -287,11 +287,11 @@ export function UpgradePlanModal({
</DataroomTrialModal>
) : (
<a
href="https://cal.com/marcseitz/papermark"
href="https://dataroom.hanzo.ai/contact"
target="_blank"
className="underline-offset-4 transition-all hover:text-gray-800 hover:underline hover:dark:text-muted-foreground/80"
>
Looking for Papermark Enterprise?
Looking for Hanzo Dataroom Enterprise?
</a>
)}
</div>
@@ -5,10 +5,10 @@ import { useEffect, useMemo, useState } from "react";
import React from "react";
import { useTeam } from "@/context/team-context";
import { getStripe } from "@/ee/stripe/client";
import { Feature, PlanEnum, getPlanFeatures } from "@/ee/stripe/constants";
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
import { PLANS } from "@/ee/stripe/utils";
import { getStripe } from "@/lib/billing/legacy/client";
import { Feature, PlanEnum, getPlanFeatures } from "@/lib/billing/legacy/constants";
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
import { PLANS } from "@/lib/billing/legacy/utils";
import {
CheckIcon,
CircleHelpIcon,
+4 -4
View File
@@ -5,10 +5,10 @@ import { useEffect, useMemo, useState } from "react";
import React from "react";
import { useTeam } from "@/context/team-context";
import { getStripe } from "@/ee/stripe/client";
import { Feature, PlanEnum, getPlanFeatures } from "@/ee/stripe/constants";
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
import { PLANS } from "@/ee/stripe/utils";
import { getStripe } from "@/lib/billing/legacy/client";
import { Feature, PlanEnum, getPlanFeatures } from "@/lib/billing/legacy/constants";
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
import { PLANS } from "@/lib/billing/legacy/utils";
import { CheckIcon, CircleHelpIcon, Users2Icon, XIcon } from "lucide-react";
import { useAnalytics } from "@/lib/analytics";
+3 -3
View File
@@ -5,9 +5,9 @@ import { useRouter } from "next/router";
import { Dispatch, SetStateAction, useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum, getPlanFeatures } from "@/ee/stripe/constants";
import { getPriceIdFromPlan } from "@/ee/stripe/functions/get-price-id-from-plan";
import { PLANS } from "@/ee/stripe/utils";
import { PlanEnum, getPlanFeatures } from "@/lib/billing/legacy/constants";
import { getPriceIdFromPlan } from "@/lib/billing/legacy/functions/get-price-id-from-plan";
import { PLANS } from "@/lib/billing/legacy/utils";
import Cookies from "js-cookie";
import { CheckIcon, Sparkles, X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
+2 -2
View File
@@ -1,7 +1,7 @@
"use client";
import { ConversationListItem as ConversationListItemEE } from "@/ee/features/conversations/components/dashboard/conversation-list-item";
import { ConversationMessage as ConversationMessageEE } from "@/ee/features/conversations/components/shared/conversation-message";
import { ConversationListItem as ConversationListItemEE } from "@/features/conversations/components/dashboard/conversation-list-item";
import { ConversationMessage as ConversationMessageEE } from "@/features/conversations/components/shared/conversation-message";
export function ConversationListItem(props: any) {
return <ConversationListItemEE {...props} />;
@@ -1,6 +1,6 @@
import { useState } from "react";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
FileJson,
FileSlidersIcon,
@@ -1,6 +1,6 @@
import { useState } from "react";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { ListOrderedIcon } from "lucide-react";
import { toast } from "sonner";
+1 -1
View File
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
Brain,
BriefcaseIcon,
@@ -191,7 +191,7 @@ export default function DataroomDocumentCard({
};
const handleCardClick = (e: React.MouseEvent) => {
if (isDragging) {
if (isDragging || menuOpen) {
e.preventDefault();
e.stopPropagation();
return;
@@ -288,7 +288,7 @@ export default function DataroomDocumentCard({
<DropdownMenu open={menuOpen} onOpenChange={handleMenuStateChange}>
<DropdownMenuTrigger asChild>
<Button
// size="icon"
onClick={(e) => e.stopPropagation()}
variant="outline"
className="z-10 h-8 w-8 border-gray-200 bg-transparent p-0 hover:bg-gray-200 dark:border-gray-700 hover:dark:bg-gray-700 lg:h-9 lg:w-9"
>
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { E164Number } from "libphonenumber-js";
import { toast } from "sonner";
import { mutate } from "swr";
@@ -263,7 +263,7 @@ export function DataroomTrialModal({
<div className="text-xs text-muted-foreground">
After the trial, upgrade to{" "}
<UpgradePlanModal clickedPlan={PlanEnum.Business}>
<button className="underline">Papermark Business</button>
<button className="underline">Hanzo Dataroom Business</button>
</UpgradePlanModal>{" "}
to continue using data rooms.
</div>
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useTeam } from "@/context/team-context";
import { useUninvitedMembers } from "@/ee/features/dataroom-invitations/lib/swr/use-dataroom-invitations";
import { useUninvitedMembers } from "@/features/dataroom-invitations/lib/swr/use-dataroom-invitations";
import {
MailCheckIcon,
MoreHorizontalIcon,
@@ -40,7 +40,7 @@ import {
import { TimestampTooltip } from "@/components/ui/timestamp-tooltip";
import { VisitorAvatar } from "@/components/visitors/visitor-avatar";
import { InviteViewersModal } from "../../../ee/features/dataroom-invitations/components/invite-viewers-modal";
import { InviteViewersModal } from "@/features/dataroom-invitations/components/invite-viewers-modal";
import { AddGroupMemberModal } from "./add-member-modal";
export default function GroupMemberTable({
@@ -2,7 +2,7 @@ import Link from "next/link";
import { Dispatch, SetStateAction, useMemo, useState } from "react";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { CircleHelpIcon, Tag } from "lucide-react";
import { toast } from "sonner";
import { mutate } from "swr";
@@ -2,8 +2,8 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useLimits } from "@/ee/limits/swr-handler";
import { PlanEnum } from "@/ee/stripe/constants";
import { useLimits } from "@/lib/billing/limits/swr-handler";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { toast } from "sonner";
import { mutate } from "swr";
+39 -6
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { FormEvent, useEffect, useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { DefaultPermissionStrategy } from "@prisma/client";
import { parsePageId } from "notion-utils";
import { toast } from "sonner";
@@ -85,7 +85,7 @@ export function AddDocumentModal({
}[]
>([]);
const teamInfo = useTeam();
const { canAddDocuments } = useLimits();
const { canAddDocuments, limits } = useLimits();
const { plan, isFree, isTrial, isPaused } = usePlan();
const { dataroom } = useDataroom();
const teamId = teamInfo?.currentTeam?.id as string;
@@ -229,7 +229,18 @@ export function AddDocumentModal({
}
if (!canAddDocuments) {
toast.error("You have reached the maximum number of documents.");
toast.error(
limits?.documents
? `You've reached your plan's document limit (${limits.usage?.documents}/${limits.documents} documents). Upgrade your plan to upload more.`
: "You have reached the maximum number of documents.",
{
action: {
label: "Upgrade",
onClick: () => router.push("/settings/billing"),
},
duration: 8000,
},
);
return;
}
@@ -415,7 +426,18 @@ export function AddDocumentModal({
}
if (!canAddDocuments) {
toast.error("You have reached the maximum number of documents.");
toast.error(
limits?.documents
? `You've reached your plan's document limit (${limits.usage?.documents}/${limits.documents} documents). Upgrade your plan to upload more.`
: "You have reached the maximum number of documents.",
{
action: {
label: "Upgrade",
onClick: () => router.push("/settings/billing"),
},
duration: 8000,
},
);
return;
}
@@ -566,7 +588,18 @@ export function AddDocumentModal({
}
if (!canAddDocuments) {
toast.error("You have reached the maximum number of documents.");
toast.error(
limits?.documents
? `You've reached your plan's document limit (${limits.usage?.documents}/${limits.documents} documents). Upgrade your plan to upload more.`
: "You have reached the maximum number of documents.",
{
action: {
label: "Upgrade",
onClick: () => router.push("/settings/billing"),
},
duration: 8000,
},
);
return;
}
@@ -759,7 +792,7 @@ export function AddDocumentModal({
<>
Upload larger files and more{" "}
<Link
href="https://www.papermark.com/help/article/document-types"
href="https://dataroom.hanzo.ai/help/article/document-types"
target="_blank"
className="underline underline-offset-4 transition-all hover:text-muted-foreground/80 hover:dark:text-muted-foreground/80"
>
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { TeamContextType } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import {
BetweenHorizontalStartIcon,
ChevronRight,
+2 -2
View File
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { useTeam } from "@/context/team-context";
import { DocumentAIDialog } from "@/ee/features/ai/components/document-ai-dialog";
import { PlanEnum } from "@/ee/stripe/constants";
import { DocumentAIDialog } from "@/features/ai/components/document-ai-dialog";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { Document, DocumentVersion } from "@prisma/client";
import {
ArrowRightIcon,
+2 -2
View File
@@ -147,7 +147,7 @@ export default function FolderCard({
};
const handleCardClick = (e: React.MouseEvent) => {
if (isDragging) {
if (isDragging || menuOpen) {
e.preventDefault();
e.stopPropagation();
return;
@@ -258,7 +258,7 @@ export default function FolderCard({
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button
// size="icon"
onClick={(e) => e.stopPropagation()}
variant="outline"
className="z-10 h-8 w-8 border-gray-200 bg-transparent p-0 hover:bg-gray-200 dark:border-gray-700 hover:dark:bg-gray-700 lg:h-9 lg:w-9"
>
+18 -28
View File
@@ -1,13 +1,9 @@
import { useEffect, useRef, useState, type ElementType } from "react";
import { type ElementType, useEffect, useRef, useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlanEnum } from "@/ee/stripe/constants";
import { PlanEnum } from "@/lib/billing/legacy/constants";
import { LinkType } from "@prisma/client";
import {
AlertTriangleIcon,
CircleCheckIcon,
InfoIcon,
} from "lucide-react";
import { AlertTriangleIcon, CircleCheckIcon, InfoIcon } from "lucide-react";
import { toast } from "sonner";
import { useDebounce } from "use-debounce";
@@ -69,7 +65,7 @@ const STATUS_CONFIG: Record<
},
"has site": {
suffix:
"is currently pointing to an existing website. Only proceed if you're sure you want to use this domain for Papermark links.",
"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",
},
@@ -143,9 +139,9 @@ export function AddDomainModal({
return;
}
if (debouncedDomain.includes("papermark")) {
if (debouncedDomain.includes("hanzo")) {
setDomainStatus("invalid");
setStatusMessageOverride("Domain cannot contain 'papermark'.");
setStatusMessageOverride("Domain cannot contain 'hanzo'.");
return;
}
@@ -205,31 +201,27 @@ export function AddDomainModal({
return toast.error("Please enter a valid domain (e.g., example.com).");
}
if (normalizedDomain.includes("papermark")) {
return toast.error("Domain cannot contain 'papermark'.");
if (normalizedDomain.includes("hanzo")) {
return toast.error("Domain cannot contain 'hanzo'.");
}
if (saveDisabled) {
return toast.error(
statusMessageOverride ??
"Please enter a valid domain before adding.",
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,
}),
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();
@@ -370,9 +362,7 @@ export function AddDomainModal({
"Enter a valid domain to check availability."
)}
</p>
{StatusIcon && (
<StatusIcon className="h-5 w-5 shrink-0" />
)}
{StatusIcon && <StatusIcon className="h-5 w-5 shrink-0" />}
</div>
</div>
);
+2 -2
View File
@@ -87,11 +87,11 @@ function DeleteDomainModal({
<DialogTitle className="text-2xl">Delete Domain</DialogTitle>
<DialogDescription>
This will permanently delete your domain. Links using this domain will
be reset to <span className="font-medium">papermark.com</span> links.
be reset to <span className="font-medium">dataroom.hanzo.ai</span> links.
This action cannot be undone.
<div className="mt-3 text-sm font-medium text-foreground">
{domain}{" "}
<span className="text-muted-foreground"> papermark.com</span>
<span className="text-muted-foreground"> dataroom.hanzo.ai</span>
</div>
</DialogDescription>
</div>
+76
View File
@@ -4,6 +4,7 @@ import { useTeam } from "@/context/team-context";
import {
ChevronDownIcon,
CircleCheckIcon,
ExternalLinkIcon,
FlagIcon,
GlobeIcon,
MoreVertical,
@@ -12,6 +13,7 @@ import {
TrashIcon,
} from "lucide-react";
import { motion } from "motion/react";
import { toast } from "sonner";
import { mutate } from "swr";
import { cn } from "@/lib/utils";
@@ -24,6 +26,7 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { StatusBadge } from "@/components/ui/status-badge";
import { useDeleteDomainModal } from "./delete-domain-modal";
@@ -33,15 +36,21 @@ import { useDomainStatus } from "./use-domain-status";
export default function DomainCard({
domain,
isDefault,
redirectUrl: initialRedirectUrl,
redirectsAllowed,
onDelete,
}: {
domain: string;
isDefault: boolean;
redirectUrl?: string | null;
redirectsAllowed: boolean;
onDelete: (deletedDomain: string) => void;
}) {
const [showDetails, setShowDetails] = useState(false);
const [groupHover, setGroupHover] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [redirectUrl, setRedirectUrl] = useState(initialRedirectUrl || "");
const [savingRedirect, setSavingRedirect] = useState(false);
const domainRef = useRef<HTMLDivElement>(null);
@@ -65,6 +74,34 @@ export default function DomainCard({
onDelete,
});
const handleSaveRedirectUrl = async () => {
setSavingRedirect(true);
try {
const response = await fetch(`/api/teams/${teamId}/domains/${domain}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ redirectUrl: redirectUrl || null }),
});
if (!response.ok) {
const data = await response.json();
toast.error(data?.message || "Failed to save redirect URL");
return;
}
mutate(`/api/teams/${teamId}/domains`);
toast.success(
redirectUrl
? "Root redirect URL saved"
: "Root redirect URL removed",
);
} catch {
toast.error("Failed to save redirect URL");
} finally {
setSavingRedirect(false);
}
};
const handleMakeDefault = async () => {
const response = await fetch(`/api/teams/${teamId}/domains/${domain}`, {
method: "PATCH",
@@ -232,6 +269,45 @@ export default function DomainCard({
) : (
<div className="mt-6 h-6 w-32 animate-pulse rounded-md bg-gray-200 dark:bg-gray-400" />
)}
{/* Root domain redirect */}
<div className="mt-4 rounded-lg border border-gray-200 p-4 dark:border-gray-400">
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<ExternalLinkIcon className="h-4 w-4" />
Root Domain Redirect
</div>
{redirectsAllowed ? (
<>
<p className="mt-1 text-xs text-muted-foreground">
Redirect visitors who land on{" "}
<span className="font-medium">{domain}</span> to a specific
URL. Leave empty to redirect to dataroom.hanzo.ai.
</p>
<div className="mt-3 flex items-center gap-2">
<Input
type="url"
placeholder="https://example.com"
value={redirectUrl}
onChange={(e) => setRedirectUrl(e.target.value)}
className="h-9 flex-1"
/>
<Button
size="sm"
onClick={handleSaveRedirectUrl}
disabled={savingRedirect}
className="h-9 shrink-0"
>
{savingRedirect ? "Saving..." : "Save"}
</Button>
</div>
</>
) : (
<p className="mt-1 text-xs text-muted-foreground">
Root domain redirects require a{" "}
<span className="font-semibold">Business</span> plan or higher.
</p>
)}
</div>
</motion.div>
</div>
<DeleteDomainModal />
+1 -1
View File
@@ -128,7 +128,7 @@ export default function DomainConfiguration({
]}
warning={
txtVerification
? "Warning: if you are using this domain for another site, setting this TXT record will transfer domain ownership away from that site and break it. Please exercise caution when setting this record; make sure that the domain that is shown in the TXT verification value is actually the <b><i>domain you want to use on Papermark</i></b> <b><i>not your production site</i></b>."
? "Warning: if you are using this domain for another site, setting this TXT record will transfer domain ownership away from that site and break it. Please exercise caution when setting this record; make sure that the domain that is shown in the TXT verification value is actually the <b><i>domain you want to use on Hanzo Dataroom</i></b> <b><i>not your production site</i></b>."
: undefined
}
/>
+8 -8
View File
@@ -45,12 +45,12 @@ export default function CustomDomainSetup({
return (
<Html>
<Head />
<Preview>Your Papermark custom domain set up</Preview>
<Preview>Your Hanzo Dataroom custom domain set up</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Papermark</span>
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black">
{title}
@@ -83,8 +83,8 @@ export default function CustomDomainSetup({
</Text>
<ol className="list-inside list-decimal text-sm">
<li>Choose your subdomain (e.g. docs.yourcompany.com)</li>
<li>Add a CNAME record pointing to papermark.com</li>
<li>Configure the domain in your Papermark settings</li>
<li>Add a CNAME record pointing to dataroom.hanzo.ai</li>
<li>Configure the domain in your Hanzo Dataroom settings</li>
<li>Start sharing with your branded domain!</li>
</ol>
@@ -92,7 +92,7 @@ export default function CustomDomainSetup({
{hasAccess ? (
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={`https://app.papermark.com/settings/domains`}
href={`https://dataroom.hanzo.ai/settings/domains`}
style={{ padding: "12px 20px" }}
>
Set up your custom domain
@@ -100,7 +100,7 @@ export default function CustomDomainSetup({
) : (
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={`https://app.papermark.com/settings/upgrade`}
href={`https://dataroom.hanzo.ai/settings/upgrade`}
style={{ padding: "12px 20px" }}
>
Upgrade to use custom domains
@@ -113,7 +113,7 @@ export default function CustomDomainSetup({
<>
Need help? Check out our{" "}
<Link
href="https://docs.papermark.com/custom-domains"
href="https://docs.dataroom.hanzo.ai/custom-domains"
className="font-medium text-blue-600 no-underline"
>
custom domain documentation
@@ -124,7 +124,7 @@ export default function CustomDomainSetup({
<>
Want to learn more about our plans?{" "}
<Link
href="https://app.papermark.com/settings/upgrade"
href="https://dataroom.hanzo.ai/settings/upgrade"
className="font-medium text-blue-600 no-underline"
>
View pricing
+6 -6
View File
@@ -25,14 +25,14 @@ const DataRoomsInformationEmail = () => {
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Papermark</span>
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
Virtual Data Rooms
</Text>
<Text className="text-sm">Unlimited branded data rooms!</Text>
<Text className="text-sm">
With Papermark Data Rooms plan you can:
With Hanzo Dataroom plan you can:
</Text>
<ul className="list-inside list-disc text-sm">
<li>Share data rooms with one link</li>
@@ -45,9 +45,9 @@ const DataRoomsInformationEmail = () => {
<li>Use advanced link settings</li>
</ul>
<Text className="text-sm">
All about Papermark{" "}
All about Hanzo Dataroom{" "}
<a
href="https://www.papermark.com/data-room"
href="https://dataroom.hanzo.ai/data-room"
className="text-blue-500 underline"
>
Data Rooms
@@ -57,7 +57,7 @@ const DataRoomsInformationEmail = () => {
<Section className="my-8 text-center">
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={`https://app.papermark.com/datarooms?utm_source=dataroom-info&utm_medium=email&utm_campaign=20240723&utm_content=upload_documents`}
href={`https://dataroom.hanzo.ai/datarooms?utm_source=dataroom-info&utm_medium=email&utm_campaign=20240723&utm_content=upload_documents`}
style={{ padding: "12px 20px" }}
>
Create new data room
@@ -66,7 +66,7 @@ const DataRoomsInformationEmail = () => {
<Text className="text-sm">
If you require a fully customizable experience,{" "}
<a
href="https://cal.com/marcseitz/papermark"
href="https://dataroom.hanzo.ai/contact"
className="text-blue-500 underline"
>
book a call
@@ -0,0 +1,114 @@
import React from "react";
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
type DocumentChange = {
documentName: string;
};
export default function DataroomDigestNotification({
dataroomName = "Example Data Room",
documents = [
{ documentName: "Document A" },
{ documentName: "Document B" },
{ documentName: "Document C" },
],
senderEmail = "example@example.com",
url = "https://dataroom.hanzo.ai/datarooms/123",
preferencesUrl = "https://dataroom.hanzo.ai/notification-preferences?token=abc",
frequency = "daily",
}: {
dataroomName: string;
documents: DocumentChange[];
senderEmail: string;
url: string;
preferencesUrl: string;
frequency: "daily" | "weekly";
}) {
const count = documents.length;
const periodLabel = frequency === "daily" ? "today" : "this week";
return (
<Html>
<Head />
<Preview>
{`${count} new document${count !== 1 ? "s" : ""} in ${dataroomName} ${periodLabel}`}
</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mb-8 mt-4 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="font-semibold mb-8 mt-4 text-center text-xl">
{`${count} new document${count !== 1 ? "s" : ""} in ${dataroomName}`}
</Text>
<Text className="text-sm leading-6 text-black">
The following document{count !== 1 ? "s have" : " has"} been added
to <span className="font-semibold">{dataroomName}</span>{" "}
{periodLabel}:
</Text>
<Section className="my-4">
{documents.map((doc, i) => (
<Text
key={i}
className="my-1 text-sm leading-6 text-black"
>
<span className="font-semibold">{doc.documentName}</span>
</Text>
))}
</Section>
<Section className="my-8 text-center">
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={url}
style={{ padding: "12px 20px" }}
>
View the dataroom
</Button>
</Section>
<Text className="text-sm text-black">
or copy and paste this URL into your browser: <br />
{url}
</Text>
<Text className="text-sm text-gray-400">Hanzo Dataroom</Text>
<Hr />
<Section className="text-gray-400">
<Text className="text-xs">
© {new Date().getFullYear()} Hanzo AI, Inc. All rights
reserved.
</Text>
<Text className="text-xs">
You received this {frequency} digest from{" "}
<span className="font-semibold">{senderEmail}</span> because you
viewed the dataroom{" "}
<span className="font-semibold">{dataroomName}</span> on
Hanzo Dataroom. If you have any feedback or questions about this
email, simply reply to it.{" "}
<a
href={preferencesUrl}
className="text-gray-400 underline underline-offset-2"
>
Manage your notification preferences
</a>
.
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
}
+9 -10
View File
@@ -17,8 +17,8 @@ export default function DataroomNotification({
dataroomName = "Example Data Room",
documentName = "Example Document",
senderEmail = "example@example.com",
url = "https://app.papermark.com/datarooms/123",
unsubscribeUrl = "https://app.papermark.com/datarooms/123/unsubscribe",
url = "https://dataroom.hanzo.ai/datarooms/123",
unsubscribeUrl = "https://dataroom.hanzo.ai/datarooms/123/unsubscribe",
}: {
dataroomName: string;
documentName: string | undefined;
@@ -34,7 +34,7 @@ export default function DataroomNotification({
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mb-8 mt-4 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Papermark</span>
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="font-seminbold mb-8 mt-4 text-center text-xl">
{`New document available for ${dataroomName}`}
@@ -43,7 +43,7 @@ export default function DataroomNotification({
A new document{" "}
<span className="font-semibold">{documentName}</span> has been
added to <span className="font-semibold">{dataroomName}</span>{" "}
dataroom on Papermark.
dataroom on Hanzo Dataroom.
</Text>
<Section className="my-8 text-center">
<Button
@@ -58,12 +58,12 @@ export default function DataroomNotification({
or copy and paste this URL into your browser: <br />
{`${url}`}
</Text>
<Text className="text-sm text-gray-400">Papermark</Text>
<Text className="text-sm text-gray-400">Hanzo Dataroom</Text>
<Hr />
<Section className="text-gray-400">
<Text className="text-xs">
© {new Date().getFullYear()} Papermark, Inc. All rights
© {new Date().getFullYear()} Hanzo AI, Inc. All rights
reserved.
</Text>
<Text className="text-xs">
@@ -71,14 +71,13 @@ export default function DataroomNotification({
<span className="font-semibold">{senderEmail}</span> because you
viewed the dataroom{" "}
<span className="font-semibold">{dataroomName}</span> on
Papermark. If you have any feedback or questions about this
email, simply reply to it. To unsubscribe from updates about
this dataroom,{" "}
Hanzo Dataroom. If you have any feedback or questions about this
email, simply reply to it.{" "}
<a
href={unsubscribeUrl}
className="text-gray-400 underline underline-offset-2"
>
click here
Manage your notification preferences
</a>
.
</Text>
+6 -6
View File
@@ -24,12 +24,12 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
return (
<Html>
<Head />
<Preview>Upgrade to Papermark Data Rooms Plan</Preview>
<Preview>Upgrade to Hanzo Dataroom Plan</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Papermark</span>
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="font-seminbold mx-0 mb-8 mt-4 p-0 text-center text-xl">
Your Data Room plan trial expires in 24 hours
@@ -38,9 +38,9 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
Hey{name && ` ${name}`}!
</Text>
<Text className="text-sm leading-6 text-black">
Your Papermark Data Room plan trial expires in 24 hours.
Your Hanzo Dataroom Data Room plan trial expires in 24 hours.
Don&apos;t lose access to these features -{" "}
<Link href={`https://app.papermark.com/settings/billing`}>
<Link href={`https://dataroom.hanzo.ai/settings/billing`}>
upgrade today
</Link>
:
@@ -68,7 +68,7 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
<Section className="mb-[32px] mt-[32px] text-center">
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={`https://app.papermark.com/settings/billing`}
href={`https://dataroom.hanzo.ai/settings/billing`}
style={{ padding: "12px 20px" }}
>
Upgrade now
@@ -80,7 +80,7 @@ const DataroomTrial24hReminderEmail = ({ name }: TrialEndReminderEmail) => {
<span className="text-red-500 underline">disabled</span> in 24
hours.
</Text>
<Text className="text-sm text-gray-400">Marc from Papermark</Text>
<Text className="text-sm text-gray-400">Marc from Hanzo Dataroom</Text>
<Footer />
</Container>
</Body>
+4 -4
View File
@@ -29,7 +29,7 @@ const DataroomTrialEnd = ({ name }: DataroomTrialEnd) => {
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mb-8 mt-4 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Papermark</span>
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="font-seminbold mb-8 mt-4 text-center text-xl">
Your Data Room plan trial has expired
@@ -38,10 +38,10 @@ const DataroomTrialEnd = ({ name }: DataroomTrialEnd) => {
Hey{name && ` ${name}`}!
</Text>
<Text className="text-sm leading-6 text-black">
Your Papermark Data Room trial has expired.
Your Hanzo Dataroom Data Room trial has expired.
<br />
<Link
href={`https://app.papermark.com/settings/billing`}
href={`https://dataroom.hanzo.ai/settings/billing`}
className="underline"
>
Upgrade now
@@ -68,7 +68,7 @@ const DataroomTrialEnd = ({ name }: DataroomTrialEnd) => {
<Section className="my-8 text-center">
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={`https://app.papermark.com/settings/billing`}
href={`https://dataroom.hanzo.ai/settings/billing`}
style={{ padding: "12px 20px" }}
>
Upgrade now
+3 -3
View File
@@ -14,10 +14,10 @@ const DataroomTrialWelcomeEmail = ({ name }: WelcomeEmailProps) => {
<Body className="font-sans text-sm">
<Text>Hi{name && ` ${name}`},</Text>
<Text>
I am Marc, founder of Papermark. Thanks for creating a trial. Do you
need any help with Data Rooms setup?
Thanks for starting a Hanzo Dataroom trial. Do you need any help with
Data Rooms setup?
</Text>
<Text>Marc</Text>
<Text>The Hanzo Dataroom Team</Text>
</Body>
</Tailwind>
</Html>
@@ -40,7 +40,7 @@ export default function DataroomUploadNotification({
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-10 w-[465px] p-5">
<Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
<span className="font-bold tracking-tighter">Papermark</span>
<span className="font-bold tracking-tighter">Hanzo Dataroom</span>
</Text>
<Text className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black">
New File Upload
@@ -76,7 +76,7 @@ export default function DataroomUploadNotification({
<Section className="my-8 text-center">
<Button
className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
href={`https://app.papermark.com/datarooms/${dataroomId}`}
href={`https://dataroom.hanzo.ai/datarooms/${dataroomId}`}
style={{ padding: "12px 20px" }}
>
View the dataroom

Some files were not shown because too many files have changed in this diff Show More