hanzo.chat embeds hanzo.app/edit.js on every page, but script-src listed
only analytics + cloudflareinsights, so the widget was CSP-blocked. Add
hanzo.app to script-src (load) and connect-src (its /v1 calls).
Re-adds the deliberately-deleted (98d0dad34b) guest stack so an anonymous
visitor lands on the chat composer and can chat ONE free model without login,
then hits the SSO wall after a small per-IP quota. Adapted to today's code
(@hanzochat/* package names, @hanzo/iam bridge, tenant-bearer billing) — NOT a
blind revert: hanzoCloudKey.ts stays deleted.
Server (fail-closed, server-enforced):
- services/guestConfig.js: env-gated config + ephemeral guest principal/user +
guest-scoped endpoints/models builders (default model zen5-flash).
- controllers/auth/GuestController.js + POST /v1/chat/auth/guest: short-lived
guest JWT ({guest:true}, per-token random id) signed with JWT_SECRET; the
route is per-IP mint-limited (guestTokenLimiter).
- middleware/requireGuestOrJwtAuth.js: accepts guest tokens ONLY where mounted;
the jwt strategy still rejects guests everywhere else (clean 401, no CastError).
- middleware/enforceGuestScope.js: pins endpoint+model, strips
agents/tools/files/spec/preset; 403 on any other endpoint/model.
- limiters/{guestLimiters,guestMessageLimiter}.js + utils/guestClientIp.js:
per-REAL-IP quota (CF-Connecting-IP, not the token) via in-memory limiterCache
(correct at replicas:1/Recreate; Redis NOT required). Exhaust -> 402 GUEST_LIMIT.
- endpoints/custom/initialize.ts: guest principal -> shared capped GUEST_API_KEY
(KMS chat-guest-key), skipping per-user hk-/bearer billing; fail-closed if
unset. Authenticated bearer path unchanged.
- agents/index.js: guest-capable completion chain; guest-safe /chat/active poll;
AND the guest SSE read-back — GET /chat/stream/:streamId registered with
requireGuestOrJwtAuth ABOVE the strict guard (else a guest 401s reading its own
stream -> empty reply). Per-job ownership check (job.metadata.userId===req.user.id)
is unchanged: foreign=403, missing=404, no cross-principal stream leak.
- Wire models/endpoints + guest-safe bootstrap (user/convos/favorites) to
requireGuestOrJwtAuth; emit allowGuestChat/guestMessageMax in /v1/config;
balance-gate bypass for guests (no org, bounded by limiter + capped key).
Tests: restored 6 guest specs (40/40 green) + 3 guest-billing cases in
initialize.spec. Client already guest-ready (zero client changes).
Docs: LLM.md guest section corrected to in-memory-at-replicas:1 + GUEST_API_KEY.
Do NOT deploy: red security review next, then cto native build + CR image pin.
Complete the chat auth migration onto @hanzo/iam. The @hanzo/iam SPA runs
Authorization-Code + PKCE entirely in the browser; a new backend session-bridge
turns the IAM-verified identity into the EXISTING Chat session, so live login,
reload-persistence, the 401->refresh interceptor, and the cloud-agent
on-behalf-of (OBO) flow all keep working. This is a session-bridge, not a blind
swap — the change is purely additive server-side.
Backend
- POST /oauth/iam/session (api/server/controllers/auth/iamSession.js): accepts
{ accessToken, idToken } from the SPA, JWKS-validates the id_token against
hanzo.id (issuer + RS256 signature + expiry), reconciles the Mongo User by
`sub` (openidId, provider=openid — create/migrate via findOpenIDUser), issues
the existing Chat session via setAuthTokens (refresh cookie + Mongo Session +
token_provider=chat + Chat JWT), and persists the id_token server-side
(persistOpenIDTokensToSession) so resolveTenantBearer can forward it on OBO
cloud calls. IAM tokens never become the app bearer; the id_token stays
server-side.
- api/server/services/iamToken.js: standalone JWKS verifier (the openIdJwtStrategy
JWKS path promoted to a callable), self-contained via OIDC discovery of
OPENID_ISSUER — no dependency on the Passport OpenID login strategy.
Frontend
- OAuthCallback: after IAM.handleCallback(), POST the token to /oauth/iam/session
and set the returned Chat JWT (dispatchTokenUpdatedEvent). Fixes the prior
broken callback that set the IAM token as the app bearer (rejected by
requireJwtAuth) and created no server session.
- data-provider: add the iamSession() endpoint + export.
Deps
- @hanzo/iam ^0.13.1 -> ^0.13.8 (relocked).
Verification
- New unit tests green (iamSession 10, iamToken 5); AuthService + CloudAgentsClient
green (52 in-scope tests pass). Typecheck introduces 0 new errors.
- verifyIamToken integration-tested against LIVE hanzo.id JWKS: forged (real kid,
wrong key), unknown-kid, and empty tokens all rejected.
The server-initiated /oauth/openid routes are kept as a dormant, unreferenced
fallback (the SPA uses only the bridge); their deletion is deferred until the
full browser login flow is verified live end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the script.js/data-website-id snippet in client/index.html <head> with the ONE canonical <script async src=https://analytics.hanzo.ai/hz.js data-site="hanzo.chat">. Drops the %VITE_ANALYTICS_SITE_ID% build placeholder (site key is now the static domain).
The 'logo single-source' change added @hanzo/logo@^1.0.13 to client/package.json
but did not update pnpm-lock.yaml, so the Docker build's `pnpm install --frozen-lockfile`
has failed (ERR_PNPM_OUTDATED_LOCKFILE) on every build since — chat main was
unbuildable. Regenerated with the pinned pnpm@10.27.0 (--lockfile-only). @hanzo/logo
is a public npm package; peer-dep warnings are pre-existing (legacy-peer-deps).
Removes the upstream LibreChat brand from user-visible + internal surfaces (pt-PT
locale string, MCP trust-warning template, code comments, the libreChatProxy var,
the LIBRE_CHAT_DOCKER_TAG build-script env, README prose). App UI was already
Hanzo-branded; this cleans the residue.
KEPT deliberately:
- LICENSE / packages/data-schemas/LICENSE 'Copyright (c) … LibreChat' — the MIT
license REQUIRES the attribution; stripping it would violate the license.
- All LibreOffice references (packages/api/src/files/documents/libreoffice.ts) —
that is the office-suite doc-preview dependency, NOT LibreChat; removing it
breaks document preview.
Prepend enso to the Hanzo endpoint (customOrder 0) models.default list, so a new
conversation opens on enso — the balanced orchestrated tier that routes each
request to the best-fit model. zen5-* stay selectable; titleModel/summaryModel
stay zen5-flash (fast, in-catalog title generation, unrelated to the composer).
GATED — config mirror only, do not deploy: the authoritative prod list is the
universe chat-config ConfigMap (repo chat.yaml mirrors it), so go-live is a
ConfigMap update gated on enso being served on api.hanzo.ai (ENSO_URL in ai) and
enso-ultra preservation validation.
Stop re-typing the block-H path data — import MARK_PATHS/MARK_VIEWBOX from
@hanzo/logo (the one geometry home). Byte-identical mark, same props
(className/size/currentColor), 4 consumers unaffected. Needs @hanzo/logo
published install to update the pnpm lock (deploy-gate); scoped tsc of the
changed file green against real react + logo types.
Re-implement the IAM-only rip on the re-rooted @hanzochat main (the old
auth/iam-only branch shares no history and is unmergeable). @hanzo/iam owns
every credential step via redirect-PKCE to hanzo.id; /auth/callback completes
the token exchange.
Client
- iam.ts: one always-configured IAM SDK singleton (env + prod defaults
hanzo.id / hanzo-chat), redirect_uri ${origin}/auth/callback
- Login.tsx: single "Log in with Hanzo" -> signinRedirect(); auto-redirect
on mount unless ?redirect=false
- drop LoginForm, Registration, ResetPassword, RequestPasswordReset,
SocialButton, SocialLoginRender (+ specs); drop the register / forgot /
reset routes
- GuestLimitDialog / LandingPage / login.ts: login = IAM signinRedirect
Server
- delete local + third-party passport strategies (local, apple, discord,
facebook, github, google, ldap, saml) and their specs
- keep jwt / openid / openidJwt strategies for IAM JWT validation
- socialLogins: Hanzo IAM (OpenID Connect) only
- auth routes: drop /login, /register, /requestPasswordReset, /resetPassword
- remove dead requireLocalAuth / requireLdapAuth middleware + LoginController
Build
- give the @hanzochat/api rollup enough heap (cross-env
NODE_OPTIONS=--max-old-space-size=8192) to fix the OOM exit 134
- raise the Dockerfile heap default to 8192 to match
The community showcase lists ONLY apps whose repository lives on
git.hanzo.ai. git.ts is the one place that decides "lives on Hanzo Git"
for both sides: isHanzoGitUrl (client link guard) and HANZO_GIT_URL_PATTERN
(server store query, matched case-insensitively). The marketplace query
pins source=git.hanzo.ai; the plain builder list is unaffected.
The showcase decides "lives on git.hanzo.ai" in one place: git.ts. It now
also exports HANZO_GIT_URL_PATTERN (built from HANZO_GIT_HOST) for the
server store query; the agents list handler matches it case-insensitively.
Server and client (isHanzoGitUrl) now accept the same URLs -- http(s),
exact host, any case -- so a repo stored with non-canonical scheme or
casing is no longer silently dropped from the showcase.
The agent/app marketplace is the community showcase. Apps are built in
hanzo.app, which clones each into git.hanzo.ai; scope the showcase to those
apps and surface only their git.hanzo.ai repositories.
- data-provider/git: one source of truth for the host (git.hanzo.ai), the
canonical base URL, and isHanzoGitUrl() — shared by the marketplace query,
the server-side list filter, and the client link guard.
- Agent record gains `repository` (its git.hanzo.ai clone); the list projection
returns it.
- getMarketplaceAgents pins source=git.hanzo.ai; getListAgentsHandler restricts
the result to agents whose `repository` is on git.hanzo.ai only when that
source is set, so the builder's plain agent list is unaffected.
- Agent detail renders a repository link guarded by isHanzoGitUrl, so only
git.hanzo.ai URLs are ever shown.
Producer follow-on (cross-repo): hanzo.app sets `repository` to the git.hanzo.ai
clone URL when it builds an app.
Directive 1 — every "build an app" affordance now DIRECTS to hanzo.app
(chat stays chat; hanzo.app is the builder sibling). One destination via
openAppBuilder/buildAppUrl:
- Composer BuildAppButton opens hanzo.app seeded from the composer text
(was: toggled an inline placeholder pane).
- Landing starter row gains a "Build an app" link-out chip (plain-model
default only; agent/assistant starters untouched).
- /build deep-link redirects to hanzo.app; the /build command + message
action already handed off.
- Retired the inline build shell (BuildPreviewPane, buildMode atom,
ChatView split) — it only ever placeheld before the same handoff.
Directive 2 + landing refresh — LandingPage (logged-out front door):
- Pricing block -> unified plans $20/$100/$200/mo, "shared AI usage across
every Hanzo app (chat, app builder, API) + pay-as-you-go", each linking
to hanzo.ai/pricing; kept the real $5 free-credit line.
- Zen showcase -> current house families (enso, enso-flash, zen5,
zen5-coder, zen3-omni); dropped param/context chips (no repo source to
cite). Demo terminal zen4-coder -> zen5-coder.
- Third-party wall -> models served today (Claude Opus 4.8, Claude 5
Sonnet, GPT-5.2, DeepSeek V4, Qwen3.5, Llama 4).
Directive 3 — dead help/docs links repointed to the live docs
(docs.hanzo.ai/chat; hanzo.ai/docs/chat is 404): landing nav/hero/CTA/
footer, AdminSettingsDialog, ApiKeyDialog, Usage routing link, and the
helpAndFaqURL server default.
Directive 4 — useResumableSSE surfaces a completions 5xx that carries a
structured error body immediately instead of spinning through 5 reconnect
attempts (~30s) then a generic message. sse.js always sets responseCode on
an HTTP failure, so a 5xx-with-body previously missed the surface branch;
bodyless/non-JSON failures still reconnect (transient-drop resilience). +2 tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Match the hanzo.ai hero on the new-conversation empty state, reusing the
ONE existing composer + submit path (presentation only, no second input).
- Landing: default heading is now "What can I help with?" (localized
com_ui_landing_title); admin customWelcome still wins. Short headings
scale up to a hero size (text-3xl -> md:text-5xl), responsive.
- ChatForm: derive one isLanding memo; reuse it to center (existing logic,
de-duplicated) AND enlarge the composer on the empty state only (taller
initial height + text-base md:text-lg -> bigger placeholder/pill).
- ConversationStarters: reshape the existing chips into a centered
monochrome pill row; clicking now PREFILLS the composer via the canonical
submitPrompt (arms activePrompt, honors autoSendPrompts) instead of a
second send path. Honest generic defaults: Summarize / Write code /
Explain / Brainstorm. Agent/assistant author starters still flow through.
Monochrome throughout via existing brand tokens (text/surface/border-*);
SendButton is already the circular arrow-up. No new deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'Fix' affordance on AI-generated images (hover pill on the message
thumbnail and an action in the fullscreen dialog) that attaches the image
to the composer as a reference file and seeds a 'Fix this image: ' prompt,
so the user only describes the fix.
Three attach paths, all reusing the existing file pipeline:
- Fix: attaches the generated image by reference (file_id, no re-upload) via
useAttachImage, injecting a completed ExtendedFile (attached: true) into the
composer file map — the send path forwards it verbatim as multimodal content.
- Drag-and-drop: plain image/file drops with no tool-resource choice now attach
straight into the current conversation instead of a single-option dead-end modal.
- Attach a previous image: new attach-menu item opens a picker of the current
conversation's images (collectConversationImages) and attaches by reference.
Pure helper collectConversationImages/resolveImageUrl unit-tested (10 cases).
i18n keys added to the default locale. No backend changes.
Co-authored-by: Hanzo AI <ai@hanzo.ai>
The content-free router-training feedback transport now lives once, in
`@hanzo/ai` `sendFeedback` (v0.2.1). `client/src/utils/rewardSignal.ts` becomes
a THIN adapter: it keeps the exact public `sendRewardSignal(requestId, signal,
rating?)` the message-actions and model-selector surfaces wire to, plus the
local `VITE_HANZO_FEEDBACK` opt-out, and delegates emission to the SDK with the
chat-specific `baseUrl` (VITE_HANZO_API_URL, cross-origin gateway) and the
end-user bearer lifted from axios defaults. The SDK owns the whitelisted
`{request_id, signal, rating?}` body, the dedupe, and the fire-and-forget
transport — so no prompt/response/code can transit, exactly as before.
The existing spec passes untouched (behavior preserved end-to-end through the
SDK; jsdom has no sendBeacon, so the SDK falls back to the same keepalive fetch
with the same headers).
Tests: client/src/utils/rewardSignal.spec.ts — 13/13 passed.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
Co-authored-by: hanzo-dev <dev@hanzo.ai>
Client-side product analytics via @hanzo/capture: pageviews, identify(user.id), and CHAT_STARTED/CHAT_MESSAGE_SENT with endpoint+model identifiers only — no message content or PII. Lockfile + tsconfig subpath mapping included.
Emit CONTENT-FREE reward signals to `POST {VITE_HANZO_API_URL}/v1/feedback` so
the router training loop gets production feedback. Payload carries ONLY
{request_id, signal, rating?} — never any prompt/response/tag/text/filename/code.
Fire-and-forget: never blocks UX, silent no-op on any failure.
The routing ledger keys each RoutingEvent on the upstream gateway response id
(chatcmpl-…/msg_…). The chat client previously held only locally-minted UUIDs,
so capture the REAL id server-side and thread it to the client as one field:
- server: ModelEndHandler captures `data.output.response_metadata.id ?? .id`
(the last CHAT_MODEL_END = the visible answer) into a shared `runMetadata`;
AgentClient surfaces it via `this.metadata` → `feedbackRequestId` on the saved
response message, riding the SSE final event + DB save to the client.
- schema: add optional `feedbackRequestId` to tMessageSchema (zod), IMessage
(mongo type) and the mongoose message schema (persists; SQLite keeps it in its
JSON doc blob automatically). Absent id ⇒ signal no-ops (never fabricated).
Client signals wired via new `client/src/utils/rewardSignal.ts`:
- thumbs up/down → up/down; regenerate → regenerate; copy → up (weak positive);
model-switch right after a response → switch (last assistant msg's id).
Dedicated cross-origin credentialed fetch (keepalive) to the Hanzo API — NOT the
same-origin chat backend — forwarding the end-user bearer for per-org attribution.
Honors local opt-out `VITE_HANZO_FEEDBACK=0|false|off` (server-side org/user
training opt-in is the preferred enforcement). 13 unit tests, all green.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
Co-authored-by: hanzo-dev <dev@hanzo.ai>
The controller required '@librechat/data-schemas', but this fork renames all
internal packages to @hanzochat/* (214 other files use @hanzochat/data-schemas;
that upstream name resolves to nothing). Runtime-only failure (require-time), so
the green build hid it and 0.9.21 CrashLooped at CloudUsage.js:1 MODULE_NOT_FOUND
— AFTER clearing the 0.9.21 zod/v4 fix. One-line name fix; all other requires in
the usage backend already resolve.
Any fresh chat image built after 74954c3f8 (@hanzo/iam 0.13 lockfile regen,
which pulled openai@6.46.0) crashes on boot:
ERR_PACKAGE_PATH_NOT_EXPORTED: subpath './v4' is not defined by
"exports" in openai/node_modules/zod/package.json
openai@6.46.0 declares zod '^3.25 || ^4.0' and imports zod/v4, but pnpm
mis-resolved its peer to the older zod@3.24.4 (which has no ./v4 subpath).
The deployed 0.9.19 image predates openai@6 entering the tree, so it never
hit this — 0.9.20 was simply the first rebuild to trip the latent landmine.
Fix: a single pnpm override zod@3.24.4 → 3.25.76 (already in-tree, a
backward-compatible superset that ships the zod/v4 compat subpath),
collapsing to ONE zod. Verified: no other package floated; openai@6.46.0
now pairs zod@3.25.76. Unblocks the whole chat build pipeline, not just
the 0.9.20 usage panel.
Adds the shared Hanzo usage read to LibreChat's Usage tab, beside (not
replacing) the Mongo token-credit view. Backend CloudUsage.js proxies
cloud's GET /v1/get-cloud-usages on-behalf-of (hanzo.id bearer resolved
server-side, never to browser); client renders the CloudUsageOverview
with @hanzo/usage's headless normalizeCloudUsage over native Tailwind.
Honest when unavailable: 200 {enabled:false} hides the section.
Brings the fork completion (de-librechat, @hanzochat scope rename, @hanzo/iam
0.13 API) and the org/project/user switcher (validated active-org -> X-Org-Id
on both cloud seams) onto main for release.
The account menu shows user -> org -> project and lets a multi-org member
switch their working org. The switch pins hanzo_active_org (httpOnly),
validated server-side against the caller's own membership set (owner + groups)
via POST /v1/chat/user/active-org. resolveActiveOrg() forwards it as X-Org-Id
on both on-behalf-of seams — the inference path (custom/initialize.ts) and
cloud agents (CloudAgentsClient) — where the gateway re-validates it in the
member's set (HIP-0026), so the header is a validated selection, never a trust
assertion. Adds Account/Console/Billing links. openidStrategy persists
organization/project/groups from the JWT claims; the user schema + TUser carry
them and getUserController serves them.
The static/IAM SPA path (client/src/utils/iam.ts, OAuthCallback.tsx) imported
BrowserIamSdk, removed when @hanzo/iam bumped 0.4->0.13 (HIP-0111); the class is
now IAM with an identical constructor config. pnpm-lock.yaml regenerated for the
@hanzochat rename. Fork now builds 5/5 tasks green.
- @librechat/agents -> @hanzochat/agents@^3.2.63 (published fork of
danny-avila/agents at github.com/hanzochat/agents; upstream remote kept
so agent-runtime fixes stay pullable)
- final sweep: zero @librechat/* package refs (chat internals + runtime)
- remove dead packages/agents residue (stale dist, no package.json)
- drop stale npm/bun lockfiles; pnpm-lock.yaml is the canonical lockfile
librechat-data-provider → @hanzochat/data-provider
@librechat/data-schemas → @hanzochat/data-schemas
@librechat/client → @hanzochat/client
(@hanzochat/api already forked; external @librechat/agents fork follows)
All imports, package.json names/deps, tsconfig path aliases, jest
moduleNameMapper, and rollup/vite build aliases updated. The data-provider
react-query subpath resolves via the exports map. Lockfiles regenerated in a
follow-on once the canonical package manager is confirmed.