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.
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>
* 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>
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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>
- 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
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.
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).
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.
- 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
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.