Compare commits

..
52 Commits
Author SHA1 Message Date
Hanzo AI ce37139741 feat(config): resolve IAM issuer per ENV from request host (devnet/testnet)
Build Docker Image / docker (push) Successful in 2m44s
console2 already resolves brand from the hostname, but mapped host->brand
only, so a non-prod host (console.devnet.hanzo.ai) resolved brand=hanzo and
fell back to the PROD issuer hanzo.id -> sign-in bounced to prod IAM.

Add envFromHost() (env label in the host: console.devnet.hanzo.ai -> devnet,
console.testnet.hanzo.ai -> testnet, console.hanzo.ai -> mainnet) and
iamUrlFor(brand, env): mainnet keeps the brand vanity apex (hanzo.id),
non-prod uses the env-scoped subdomain id.<env>.hanzo.ai. /v1 stays
same-origin so the cloud base is already env-correct.

ONE brand-agnostic image now serves mainnet/testnet/devnet with no baked
NEXT_PUBLIC_* — the documented build-time issuer override still wins if set.
+7 unit tests (envFromHost + per-env resolveConfig); full suite 155 green.
2026-06-28 15:28:48 -07:00
Hanzo AI e3e42c279f fix(security): /paas gate keys on GLOBAL admin (C1), pin session authority, block path traversal, drop x-powered-by
Red NO-GO on the v0.4.0 token re-add: the /paas gate architecture is right
(server verifies the IAM session via cloud /v1/get-account, trusts no
client-decoded token, deny-by-default) but it gated on the WRONG flag —
org-scoped `isAdmin`. The platform service token is unscoped (every tenant,
every cluster) and the proxy forwards no user identity, so the gate is the
SOLE authz: any tenant ORG admin (e.g. owner=maxpower, isAdmin=true) would
reach the GLOBAL control plane. v0.4.1, TDD (failing test -> fix -> green).

1. [C1] /paas now requires a GLOBAL platform admin, mirroring the IAM
   backend's own rule object.User.IsGlobalAdmin() == (Owner == conf.AdminOrg,
   default "admin"): owner in {admin, built-in} OR an explicit isGlobalAdmin
   verdict. org-scoped isAdmin is NO LONGER sufficient. isAdminAccount ->
   isGlobalAdminAccount (its only caller was /paas). org-admin -> 403,
   global-admin -> forward, unauth -> 401, anon -> 401.
2. [HIGH] getServerAccount authority is PINNED to server-only CLOUD_URL
   (in-cluster cloud-api), never the request origin — a Host/X-Forwarded-Host
   spoof can no longer move the session-authority endpoint (which could forge
   an admin account). No pinned authority -> fail secure (null, no network).
   getServerAccount(cookie) drops the origin arg (callers: /paas, wallet).
3. [LOW] /paas rejects `.`/`..`/empty/separator path segments (400) so a
   `..` cannot escape the platform /v1 prefix via URL normalization.
4. [LOW] next.config poweredByHeader:false — drop the X-Powered-By fingerprint.

DO NOT re-add PAAS_SERVICE_TOKEN here — the CTO re-adds it to the CR after
Red confirms a non-global org-admin gets 403.

148 unit green; tsc --noEmit clean; next build green. CLOUD_URL must be set
on the console2 deployment (in-cluster cloud-api) for the gate to function.
2026-06-28 08:43:41 -07:00
Hanzo AI 345015b745 fix(security): gate /paas proxy, server-side authz, billing IDOR/idempotency, OIDC state+PKCE, strict CSP, drop client X-Org-Id, clear prefs on logout
Build Docker Image / docker (push) Successful in 2m38s
Seven hardening fixes, TDD (failing test → fix → green); +33 unit, +13 e2e.

1. [C1] app/paas/[...path] verifies the IAM session (same-origin /v1/get-account
   via lib/auth/server) AND requires admin BEFORE attaching PAAS_SERVICE_TOKEN —
   deny-by-default: unauth 401, non-admin 403, admin reaches upstream, honest 501
   when the token is unset. Re-add PAAS_SERVICE_TOKEN to the CR after re-review.
2. Function-level authz: catch-all [...slug] denies admin-only modules to
   non-admins (Access required, module never mounts). ADMIN_PRODUCT_IDS is the
   GUI-free source of truth, drift-guarded against the catalog admin:true entries.
3. Billing top-up: userId derived from the verified session (was body.userId =
   IDOR); txHash sent as Idempotency-Key so a replay never double-credits.
4. OIDC: callback validates state against the stored value before exchange (was
   unchecked) and surfaces ?error; PKCE S256 machinery added, gated off
   (NEXT_PUBLIC_IAM_PKCE) until casibase /v1/signin forwards code_verifier.
5. Strict security headers in next.config: CSP (frame-ancestors none, object-src
   none, base-uri, scoped form-action/connect-src), HSTS, nosniff, no-referrer,
   X-Frame-Options DENY, Permissions-Policy.
6. client.ts no longer sends X-Org-Id (spoofable, not a trust boundary); org is
   derived server-side from the session.
7. signOut clears the localStorage preferences cache (no cross-user leak).

136 unit + 111 e2e green; tsc --noEmit clean; production build green.
2026-06-28 07:54:33 -07:00
Hanzo AI a318a23065 test: add E2E + unit suite (200 tests) and fix 3 bugs found via TDD
Greenfield test infrastructure for console2 (had zero tests):

- vitest 3 unit layer (test/unit, 102 tests): routing, catalog/registry
  integrity, brand config, the /v1 client envelope + REST layer, account
  anonymous handling, honest-state mapping, and the provider/model/store/app
  domain logic. Heavy GUI deps aliased to hermetic stubs (test/stubs) so the
  registry graph imports without rendering Tamagui in Node.
- Playwright E2E layer (test/e2e, 98 tests): real clicks/forms/navigation over
  auth (IAM OIDC), the catalog home, the sidebar, every enabled module + honest
  empty/403/404/503 states, ALL admin flows (IAM tabs, Audit, Secrets/KMS,
  Clusters, Kubernetes, Settings, API Keys), negative authz, forms, mobile +
  desktop viewports, and clean-console checks. Hermetic: /v1 + /paas mocked via
  route interception — never touches real prod data.
- One-command CI: `npm run test:e2e` (webServer builds + serves), `test:unit`,
  `test:all`.

Bugs fixed (TDD red -> green, each with a regression test):
- favorites: DEFAULT_PINNED named a non-existent catalog id ('billing'); the
  shell silently drops unknown pins so new users lost a pin. -> ['chat','cost'].
- least privilege: the sidebar + home rendered admin-only surfaces (IAM/KMS/
  Secrets/Audit/Clusters/Kubernetes) to every user. Added visibleCatalog(isAdmin)
  and wired it into the shell + home; server-side 403 remains the authority.
- config: brandFromHost mapped every brand's .id host except pars.id. Added it.

Behavior-neutral: nav-sidebar/page-content/pinned-section testIDs on the shell
to scope E2E assertions; DEFAULT_PINNED exported for its regression test.
2026-06-28 07:06:43 -07:00
Hanzo AI e3c4d52bb4 build: cap Node heap (NODE_OPTIONS=6144) to fix Next build OOMKill (exit 137) 2026-06-27 18:35:53 -07:00
zeekay da83b67e89 release: v0.2.0 — consolidate page-port wave 1 (o11y traces/sessions/scores, playground/evals/datasets/prompts, settings/models/api-keys)
Build Docker Image / docker (push) Successful in 2m45s
Merges port-{settings-models,prompts-evals,o11y-core}. 73 catalog entries across the 10 categories; new modules wired to real /v1 backends with honest states (o11y 503 until runtime init). tsc + next build green.
2026-06-27 18:19:45 -07:00
zeekay a13de116e8 merge: port-o11y-core (Traces/Sessions/Scores)
# Conflicts:
#	src/lib/api/index.ts
#	src/lib/products/registry.tsx
2026-06-27 18:17:56 -07:00
zeekay 725beb1d98 merge: port-prompts-evals (page-port wave 1)
# Conflicts:
#	src/lib/products/registry.tsx
2026-06-27 18:17:16 -07:00
zeekay 21c1016292 merge: port-settings-models (page-port wave 1) 2026-06-27 18:16:09 -07:00
Hanzo AI bc76e6fc76 release: v0.1.9 — REST client fix over v0.1.8 (working REST, not dead ZAP /zap WS) 2026-06-27 18:10:23 -07:00
Hanzo AI 24f9b14e69 fix(providers): use working REST client, not the dead ZAP /zap WS
ProviderListView/ProviderEditView imported ~/lib/zap, but the cloud /zap
WebSocket face is not served (edge returns SPA HTML, not a WS upgrade — per
lib/zap/client.ts LIVE STATUS), so Providers rendered 'Failed to load
providers'. Switch both to ~/lib/api (identical surface). Part of v0.1.8.
2026-06-27 17:46:04 -07:00
zeekay ac0ecfcb84 feat(console2): port prompts/playground/datasets/evals as native @hanzo/gui modules
Port the old console (Langfuse-fork) eval surfaces into console2 as native
modules on @hanzo/gui + @hanzogui/lucide-icons-2 — no antd/shadcn/tremor.
Wired to the REAL cloud /v1 backend; honest loading/empty/unavailable states
on 404/503 (NEVER fabricated prompts/datasets/scores).

- Playground (AI): GET /v1/models + POST /v1/chat/completions — fully working
  model run (system prompt, message thread, sampling params, token usage).
- Evals (Observe): POST /v1/evals/runs (real per-item run summary) +
  GET /v1/evals/scores (real scores list), Run/Scores tabs.
- Datasets (Observe): POST /v1/evals/datasets + /v1/evals/dataset-items (real
  create); forward-compatible GET list with honest unavailable card.
- Prompts (AI): forward-compatible GET /v1/prompts probe; honest deep-link card
  (no /v1 prompts route mounted yet).

Shared: ModelPicker (one way to pick a gateway model), BackendState (honest
/v1 error → card). API: lib/api/{playground,evals}.ts via the REST client.
Registry: playground+evals stubs upgraded in place (no reorder); prompts+
datasets appended. tsc --noEmit + next build both green.
2026-06-27 17:43:45 -07:00
zeekay e547011c96 feat(console2): native Traces/Sessions/Scores modules under Observe
Port the old console's observability core (Langfuse-shaped) to native
@hanzo/gui modules:
- Traces: list + detail (overview, I/O, observations, scores) at /o11y
- Sessions: list + detail (overview + traces) at /sessions
- Scores: list at /scores

All read the REAL /v1/o11y endpoints and render honest states (loading /
runtime-initializing / empty) on 503/404 — never fabricated traces or
charts. Replaces the Traces deep-link placeholder (ObservabilityModule
removed); appends Sessions + Scores entries (no reorder of existing).
tsc --noEmit and next build both pass.
2026-06-27 17:43:28 -07:00
zeekay 164f073b50 feat(console2): shared observability primitives
DRY pieces for the o11y surfaces: pure formatters (date/latency/cost/
score value/JSON), shared detail parts (DetailRow/Badge/Tags/JsonCard),
the honest RuntimeNotice (503 not-initialized / 404 unrouted / access /
error), and the list Pager. No fabricated data.
2026-06-27 17:43:16 -07:00
zeekay 6df405adcd feat(console2): o11y API client (traces/sessions/scores)
Typed client for the /v1/o11y surface over the plain-REST transport
(restGet/v1Url): list endpoints return { data, meta }, detail endpoints
one object. 503/404 surface as typed ApiError so callers render honest
states. Tenancy is server-side (cookie credentials only).
2026-06-27 17:43:16 -07:00
zeekay f50dbdd9a6 feat(console2): port settings + model catalog pages as native @hanzo/gui modules
Port hanzoai/console's deep settings + models pages into console2 as native
modules wired to the REAL /v1 + /v1/iam endpoints. Honest loading/404/401/503
states everywhere — never fabricates keys, models, or settings.

New modules (registry, appended — no reorder):
- Model Catalog (AI): real GET /v1/models + best-effort /v1/pricing/models
  overlay; provider filter, premium tier, $/Mtok columns. Read-only catalog
  (routing lives in Models, credentials in Providers).
- API Keys (Dev): the account's real cloud credential (accessKey/accessSecret
  from get-account), masked by default with explicit reveal + copy; honest
  "managed by gateway" state when no key material is exposed to the browser.
- Settings (Security): tabbed General / API Keys / Members / Branding. General
  reads real account (get-account) + org (/v1/iam/get-organization); Members
  reads /v1/iam/get-users; API Keys embeds the shared ApiKeysView (DRY);
  Branding shows the real per-host runtime config. Identity mutations deep-link
  to IAM rather than being re-implemented.

Shared + API:
- ui/States.tsx: one honest async-state renderer (honestError + ErrorState +
  asApiError) with per-surface copy overrides. AdminModule refactored onto it
  (removes its duplicate honestError/ErrorCard).
- api/models-catalog.ts: CloudModelApi over the REST (non-envelope) /v1/models.
- api/admin.ts: IamAdminApi.organization(name) single-org getter.

Shell: top-bar account name now links to /settings (canonical account menu).

Gates: tsc --noEmit exit 0; next build exit 0.

Skipped (no real console2 backend — porting as shells would be slop): Langfuse
feature-flags (2 internal flags), developer-tools (Langfuse-branded copy),
automations/batch-exports/integrations (tRPC+Prisma only). Members/Audit/
Secrets/LLM-connections/Billing already exist as IAM/Audit/KMS/Providers/Cost.
2026-06-27 17:40:53 -07:00
Hanzo AI b4274bda4a release: v0.1.8 (cumulative — supersedes parallel v0.1.7) 2026-06-27 17:37:11 -07:00
Hanzo AI c9adc16cad merge: integrate fix/paas-live-data (CTO v0.1.7 paas wiring) into main
main already supersedes it: same real platform contract (/v1/apps +
/v1/org/{org}/cluster) PLUS X-Org-Id (resource modules), Bot/Wallet honest
states, Clusters real-contract rewrite, and the PaaS token fix. Recording the
integration so the line is single; cutting v0.1.8 as the cumulative release.
2026-06-27 17:36:51 -07:00
Hanzo AI f30cdbbee5 fix(modules): wire every embedded module to the real /v1 backend + cut v0.1.7
Live Playwright verification surfaced real wiring bugs; fixed all in console2
(honest states everywhere, no fakes):

- client.ts: stamp X-Org-Id (brand org) on every cloud call — the provisioning
  service 403'd 'X-Org-Id required' on the direct cloud-api path. Fixes the 7
  data modules (vector/sql/kv/s3/datastore/docdb/search) → real data / empty.
- platform.ts: rework to the REAL platform contract — GET /v1/apps (apps
  inventory) + GET|POST /v1/org/{org}/cluster; drop dead /v1/clusters + k8s
  passthrough. Status = real health board; Kubernetes = real workloads per
  cluster; Clusters = real dedicated-DOKS list (honest empty).
- platform/state.tsx: upstream 401/403 → honest 'not configured'.
- BotModule: /v1/bot/health 404 → honest 'not routed on this host'.
- WalletModule: /v1/billing/balance 404 → honest 'not available'.
- StatusTag: understand platform health verdicts (green/yellow/red).

typecheck + build clean. Operator CR token repointed (universe) to the correct
paas-console-token (the old hanzo-paas/MASTERTOKEN is rejected by platform).
2026-06-27 17:34:03 -07:00
zeekay a307d64b07 fix(paas): wire Clusters/Kubernetes/Status to real platform /v1 surface
Build Docker Image / docker (push) Successful in 2m36s
The PaaS modules targeted an assumed platform surface (/v1/clusters,
/v1/org/{org}/cluster/{id}/k8s/{kind}) that does not exist — the live
platform.hanzo.ai/v1 REST API serves /v1/org/{org}/cluster (org-scoped
dedicated clusters) and /v1/apps (the workload/drift board). Re-point the
single data layer (src/lib/api/platform.ts) at the real endpoints:

- listClusters → /v1/org/{org}/cluster (unwrap {clusters}); org from config.
- add AppsApi.listApps → /v1/apps (unwrap {apps}) — the live deploy data.
- drop the dead k8s-browse client (no backend) + CLUSTER_ROUTES guess.

Status + Kubernetes now render the real /v1/apps workloads (cluster,
namespace, image, health) instead of gating on a non-existent k8s-browse
behind a (for hanzo) empty dedicated-cluster list. StatusTag tones the
drift-board health (healthy/warning/down). Auth is unchanged: the /paas
proxy already sends Authorization: Bearer; the fix is the token VALUE
(operator CR → paas-console-token) + these paths.
2026-06-27 17:28:37 -07:00
zeekay c32bb30154 docs: correct catalog home comment for the 3-state enablement model 2026-06-27 16:44:35 -07:00
zeekay c27a62d271 catalog: canonical 10-category Open AI Cloud (GCP-compatible) + all-services Status
Build Docker Image / docker (push) Successful in 2m33s
Rebuild the product catalog to the canonical ten categories, in order:
AI · Compute · Data · Network · Security · Dev · Deploy · Observe · Web3 · Apps.
66 entries; each names its Google Cloud equivalent. Honest 3-state enablement:
enabled (23, in-console modules) / external (16, live Hanzo surfaces) / soon (27).
Every real working module is preserved (recategorized, not broken).

Add Status (Observe): live health of every Hanzo service across clusters, from
REAL data only — composes PlatformApi.listClusters + KubernetesApi deployments
over the /paas control plane, with honest not-configured/unavailable/empty
states and no fabricated dots.

Decomplect: one coming-soon surface for all soon leaves; delete the dead
duplicate PaaS client (PlatformModule + lib/paas) and the unused coming-soon
factory. Release v0.1.6.
2026-06-27 16:43:30 -07:00
zeekay 102a37a48c release: v0.1.5
Build Docker Image / docker (push) Successful in 4m57s
Phases 1-5: nine-category catalog (mirrors hanzo.ai), in-console admin
(Identity/Secrets/Audit), Kubernetes workloads browser + real Clusters over
/paas, and an honest Observability category. tsc --noEmit + next build green.
2026-06-27 16:10:25 -07:00
zeekay 6beb670744 o11y: honest Observability category (traces/evals/prompts)
Phase 5 — add the Observability category with truthful entries, no fake charts:

- Observability (/o11y): console-native module that probes the REAL /v1/o11y
  runtime and reports status (online / not-initialized 503 / not-routed 404 /
  access / error), deep-links to the full observability surface for traces,
  evals, and prompts, and marks the native in-console browser as coming
  (HIP-0106). Never renders placeholder telemetry.
- Insights (external) + Analytics (relocated here) round out the category.

This is the staged first step for the largest old-console surface; the deeper
native port is honestly deferred, not faked.
2026-06-27 16:06:34 -07:00
zeekay 82e81ddf75 k8s: Kubernetes workloads browser + real Clusters over /paas
Phase 4 — one platform transport: route ALL control-plane calls through the
same-origin /paas proxy (server-side token injection, no CORS, honest 501
when unset) instead of direct cross-origin platform.hanzo.ai.

- platform.ts: clusters now go via /paas; add KubernetesApi over
  /v1/org/{org}/cluster/{id}/k8s/{deployments,pods,services,ingresses,events,crs}.
- KubernetesModule: cluster picker + resource tabs, defensive per-kind columns,
  honest loading/not-configured/backend-unavailable/error/empty states.
- ClustersModule: flip 'soon' -> real; render the shared honest state card for
  not-configured (501) / backend-unavailable (404).
- New shared platform/state.tsx interprets /paas errors one way.
2026-06-27 16:01:25 -07:00
zeekay 1ad8ff3fa0 admin: in-console Identity, Secrets (KMS), and Audit modules
Phase 3 — wire the console to the existing identity/secrets subsystems over
the canonical /v1 surface (HIP-0111), each an honest module:

- Identity (/iam): tabbed Organizations / Users / Roles (RBAC) over Hanzo
  IAM /v1/iam/get-{organizations,users,roles} (casdoor envelope). One generic
  AdminListView drives all three; 404/401/empty are explicit honest states.
- Audit (/audit): identity & access event log over /v1/iam/get-records.
- Secrets (/kms): KMS is zero-knowledge (encrypted names+values, token/ZAP
  auth, no list-values endpoint) — the module states the model, probes the
  real /v1/kms surface to report reachability, and deep-links to the KMS
  console. It never fabricates a secret table.

iam/kms flip from external links to modules; ext.iam/ext.kms removed.
2026-06-27 15:56:31 -07:00
zeekay 631dc417b2 catalog: mirror hanzo.ai's nine product categories
Replace the ad-hoc AI/Data/Apps/Identity/Infrastructure/Commerce grouping
with the exact nine categories + order from the marketing site product
dropdown (navigation-data.ts productsNav): AI & Agents, Developer, Apps,
Compute, Data, Async, Platform, Observability, Web3. One taxonomy across
every Hanzo surface. Empty groups don't render (catalogByCategory skips them).
2026-06-27 15:50:57 -07:00
zeekay 57371b1c9d feat(console2): Wallet & HUSD top-up module + verify-and-record endpoint; v0.1.4
Build Docker Image / docker (push) Successful in 2m47s
- WalletModule (Commerce): connect a non-custodial wallet on Hanzo Mainnet
  (36900) via ethers/EIP-1193, show wallet HUSD + cloud credit balances, and
  top up credit with HUSD. Honest states throughout (no wallet, HUSD greenfield,
  chain unreachable, endpoint unconfigured) — never a fabricated balance.
- src/lib/wallet/hanzo-evm.ts: one canonical Hanzo Mainnet + HUSD definition
  (ethers v6), env-overridable RPC; HUSD address is public (NEXT_PUBLIC), never
  a secret.
- src/lib/api/wallet.ts: cloud balance via the real GET /v1/billing/balance, and
  recordWalletTopup → the console's own POST /billing/topup/wallet.
- app/billing/topup/wallet/route.ts: server route (mirrors /paas) — verifies the
  HUSD transfer on-chain, then records to commerce as a husd crypto payment and
  credits the balance. Hosted here because billing.hanzo.ai is a static export
  and commerce is owned elsewhere. Server-only config (KMS, never NEXT_PUBLIC).
- registry: wallet entry; api/index: WalletApi export. Adds ethers 6.17.0.
2026-06-27 14:56:35 -07:00
zeekay 0076f411b0 console2: per-product discover interstitials (docs + GitHub OSS + dividends); v0.1.3
Build Docker Image / docker (push) Successful in 2m29s
'Interstitial screens to discover guides/docs/how-tos + link to GitHub OSS for
each product' + the OSS-gets-paid hook:
- /discover/<id> route + ProductInterstitial: identity + open/get-started CTA,
  Docs & guides + Open-source (GitHub) link cards, and an 'Open source gets paid'
  card stating the model (25% of cloud revenue -> OSS contributors, computed from
  each deployment's SBOM, settled on-chain in HUSD) with Contribute + OSS-dividends
  links. Catalog cards gain a Learn-more (info) affordance; non-enabled Get-started
  routes to the interstitial.
- OSS_PROGRAM constant (one source of truth: 25% / HUSD / SBOM / dividends dashboard),
  aligned with hanzo.ai sbomRevenueConfig + the commerce contributor/payout system.

tsc clean; next build green (/discover/[id] live).
2026-06-27 14:22:42 -07:00
zeekay 9b04ea848c ci: semver image tags only (no sha, no :latest); v0.1.2
Build Docker Image / docker (push) Successful in 2m32s
Per directive 'use proper semver for all, no sha': build-image.yml now tags
ghcr.io/hanzoai/console2 with the semver version only — a v* git tag publishes
that exact version, a main push publishes v<package.json version> (bump to
release). Drops the sha-<sha7> and floating :latest tags. Bump 0.1.1 -> 0.1.2
(GCP-grade resource detail + Plans & Pricing + Team launcher).
2026-06-27 13:58:21 -07:00
zeekay e30e9c7b3e console2: GCP-grade resource detail + Plans & Pricing + Team launcher
The 'manage/create databases ala GCP' + 'discover, enable, pay' surface,
all on REAL backend contracts (no invented controls):

- ResourceModule gains an instance DETAIL view (list+detail in one component,
  mirroring ProvidersModule's params.name): click a resource -> GET /v1/<kind>/<name>
  overview (status/kind/endpoint/user/db/created) + connection guidance + danger-zone
  delete. New resourceRoutes() helper binds index + :name to one instance (DRY);
  the 7 data products (sql/vector/datastore/kv/search/s3/docdb) use it. Resources
  are serverless/create-by-name (POST /v1/<kind> takes only {name}) — no fake
  tier/size/region knobs.
- PlansModule: GCP-style Plans & Pricing on the live rate card (GET /v1/pricing):
  per-tier cards (vCPU/RAM/SSD/transfer, monthly+hourly, feature bullets, free/popular
  badges) + block-storage metering. Paying delegates to config.billingUrl (the one
  money surface, never reimplemented). New typed PlansApi (plans.ts).
- Team launcher: hanzo.team catalog entry (Apps, external team.hanzo.ai).

tsc --noEmit clean; next build green (9/9).
2026-06-27 13:26:41 -07:00
zeekay f057352fc3 feat(console2): Bot module — in-console /v1/bot status + operator deep-links
Flip the existing 'bot' catalog entry from an external link to an in-console
module. New BotModule reports live gateway status from /v1/bot/health (via a
small BotApi on the REST layer, since the bot speaks plain JSON, not the
casibase envelope) and deep-links the operator surfaces (bot home, control UI,
docs, source). Backend (hanzoai/bot -> bot-gateway) is already routed at
/v1/bot/* by the unified gateway.
2026-06-27 12:45:58 -07:00
zeekay a0db49f282 fix(console2): cloud fallback → api.hanzo.ai (gated gateway), not the SPA host
Found via testing: cloud.hanzo.ai serves the SPA catch-all (200 text/html for any /v1 path), NOT the backend; the real /v1 is behind the unified gateway api.hanzo.ai (hanzoai/ingress→hanzoai/gateway v2.13.0 → cloud / separate services / per-org k8s, rate-limited+gated+priced). Browser uses same-origin /v1 (each console host's ingress proxies to the gateway); this SSR fallback now points at the gateway.
2026-06-27 12:21:14 -07:00
zeekay c09943eb08 feat(console2): one image, multi-brand by hostname (hanzo/lux/zoo)
Resolves the brand at RUNTIME from the request hostname (console.hanzo.ai→hanzo, console.lux.cloud→lux, console.zoo.cloud→zoo). Tenancy: ONE cloud /v1 backend serves all orgs (same-origin per host → first-party cookie, no CORS), each brand authenticates against its OWN live IAM (hanzo.id / lux.id / zoolabs.id / pars.id; client_id <org>-cloud per HIP-0111). config is a brand-aware Proxy so the /v1 client + IAM SDK go per-host with no consumer changes; cloudUrl is same-origin (window.origin); NEXT_PUBLIC_* still override. Dockerfile + CI no longer bake NEXT_PUBLIC_* (that pinned one brand) → one brand-agnostic image (sha-<sha7>+latest). Fixed a 'Hanzo' subtitle brand-leak. Verified: tsc clean + next build green. NOTE: cloud backend must accept all brand issuers/auds; each console host's ingress must proxy /v1 to the backend + register the redirect URI in that brand's IAM app.
2026-06-27 12:11:09 -07:00
Hanzo AI 0e2ee1f394 feat(console2): brand polish — favicon, monochrome, sized loader, social sign-in
- favicon: canonical hanzo.app ▼/H mark via app-router icon files
  (favicon.ico, icon.svg, apple-icon.png); viewport themeColor #000000
- loader: monochrome HanzoMark ~40–48px centered (not viewport-filling),
  replacing the oversized @hanzo/gui Spinner in AuthGate + OAuth callback
- monochrome: black/white/grey chrome, white (theme="light") primary
  buttons; drop every blue/green/red/yellow/violet theme + $color accent
  across products, status tags, dashboard badges, default store color
- sign-in: dedicated GitHub + Google buttons (provider_hint) + Hanzo ID,
  all via hanzo.id OIDC (client_id=hanzo-cloud); IAM owns provider OAuth,
  console never reconstructs github.com/accounts.google.com URLs
2026-06-26 19:22:26 -07:00
Hanzo AI c6f55d8646 docs(LLM): canonical issuer hanzo.id + hanzo-cloud app + deploy reality [skip ci] 2026-06-26 18:27:53 -07:00
Hanzo AI 14519c41c5 fix(ci): emit sha+latest tags via GITHUB_OUTPUT heredoc
The mainnet branch wrote a two-line tags value with $'\n', which GitHub
Actions rejects for key=value outputs ("Invalid format ...:latest") —
so every Build Docker Image run failed at 'Compute per-env config' and
no image was ever produced by CI. Use the multi-line heredoc output form
so build-push-action receives both newline-separated tags.
2026-06-26 18:15:50 -07:00
Hanzo AI 1f80429f97 fix(auth): sign-in via canonical issuer hanzo.id, not iam.hanzo.ai
NEXT_PUBLIC_IAM_URL is inlined at build time, so the deployed image sent
the browser authorize redirect to https://iam.hanzo.ai — which mints
iss=https://iam.hanzo.ai, a different issuer than the canonical
https://hanzo.id that the cloud /v1 backend validates. Sign-in dropped
on iam.hanzo.ai and never round-tripped back to console2.

- src/config, .env.example, Dockerfile ARG, mainnet CI build-arg: point
  the browser at https://hanzo.id (the value baked into the image).
- Align iamAppName/iamClientId source defaults to hanzo-cloud (was the
  stale hanzo-console / empty), matching the cloud-api backend binding
  (iamApplication / IAM_AUDIENCE = hanzo-cloud). A default build now
  targets the correct app instead of a non-existent one with no client.

App/client stay hanzo-cloud on purpose: console2 is a front-end of the
shared cloud /v1 backend, which exchanges the code and validates aud as
hanzo-cloud — it is not its own IAM principal.
2026-06-26 18:14:12 -07:00
Hanzo AI 800b3481d7 feat(console2): 10-category cloud axis + embedded PaaS, no fakes
Job 2: reorganize the catalog into the canonical 10-category CLOUD AXIS
(AI/Compute/Data/Network/Security/Dev/Deploy/Observe/Chain/Apps) so console2
reads like a cloud console. Three entry kinds (module/external/soon) => zero
dead links, zero fakes; 'soon' primitives render an honest ComingSoon overview.

Job 3: PaaS embedded natively under Deploy (PlatformModule) wired to the real
platform.hanzo.ai control plane via a same-origin /paas proxy (service token
server-side from KMS). Real apps + declared/running/drift + redeploy; honest
loading/not-configured/empty states.

Job 4: catalog honest by construction; PaaS shows only real data. No placeholder
cards, demo projects, or lorem stats.
2026-06-26 14:48:16 -07:00
Hanzo AI 7e3fa15425 Merge feat/cloud-taxonomy-10cat into main (union)
Union merge — feature's categorized catalog architecture is canonical, with
main's data/storage products + status badges folded in (lose nothing):

- registry.tsx: feature's CatalogEntry discriminated union (category + kind +
  admin + derived productModules) as the base; FOLD IN main's data/storage cloud
  products (vector, sql, datastore, kv, search, s3, docdb, base, clusters) as
  categorized 'module' entries via resourceModule/comingSoon — these are the
  console frontend for cloud's /v1 provisioning control plane. Extend
  ProductStatus to 'enabled'|'available'|'soon'|'waitlist' and add repo? so the
  folded products keep their status badges. Resolve the id 'search' collision:
  managed Search data product keeps id 'search' (owns the route, maps to the
  provisioning kind); feature's external search.hanzo.ai becomes 'ai-search'.
- DashboardShell.tsx: feature's Pinned + categorized NavRow shell; FOLD main's
  SOON/WAITLIST status badge into NavRow (feature had dropped it).
- page.tsx: feature's grouped ProductCard grid; extend StatusBadge to render
  Soon/Waitlist (blue/yellow) alongside Enabled/Available (folds main's badge).

Verify: tsc --noEmit strict CLEAN; next build green (6/6 pages). The repo's
untracked PaaS WIP (ComingSoon/PlatformModule/paas — another agent's, in neither
branch) is left untouched and excluded from this commit.
2026-06-26 14:47:11 -07:00
07db3af7fe feat(products): register full data/storage catalog + enablement status (#1)
* feat(products): register full data/storage catalog + enablement status

Adds `status` ('enabled'|'soon'|'waitlist') + `repo` to every ProductModule so
the console tracks enablement of all Hanzo cloud products in ONE place. Registers
the OSS-Google-Cloud data/storage suite as modules — Vector, SQL, Datastore, KV,
Search, S3, Base (soon) and DocDB (waitlist) — each a ZAP-native Hanzo fork mapped
to its repo, with a ComingSoon placeholder (repo link + status) until its admin
module lands. Nav + dashboard cards render the status badge.

Products → repos: Vector=hanzoai/vector (Qdrant-compat), SQL=hanzoai/sql,
Datastore=hanzoai/datastore (ClickHouse-compat), KV=hanzoai/kv (Redis-compat),
Search=hanzoai/search, S3=hanzoai/s3, Base=hanzoai/base, DocDB=hanzoai/docdb.

* feat(products): add Clusters module (shared Hanzo Cloud vs BYO DOKS)

The data/storage products (Vector/SQL/Datastore/KV/Search/S3/Base/DocDB) are
all live on hanzo-k8s via the operator. Adds the one new control-plane surface
the platform needs: Clusters — choose where workloads run (shared multi-tenant
Hanzo Cloud, or your own/Hanzo-provisioned DOKS cluster, reconciled by the same
operator). Deploy-any-repo stays under Applications.

* feat(products): working data/storage + clusters admin modules

Replace the comingSoon() placeholders for sql/vector/datastore/kv/search/s3/
docdb with a DRY resourceModule() factory over the provisioning REST contract
(POST/GET/DELETE /v1/<kind>): list, create-with-once-shown connectionString +
password reveal ("store this now"), and per-row delete. Tenancy is server-side
(gateway injects X-Org-Id), so the browser sends cookie creds only.

- lib/api/client.ts: plain-REST helpers (restGet/restPost/restDelete + v1Url)
  beside the casibase envelope path — provisioning + platform speak raw JSON /
  201 / 204 / DELETE, reusing the same cookie creds + ApiError. One transport.
- lib/api/provisioning.ts: ProvisioningApi keyed by ResourceKind.
- lib/api/platform.ts: PlatformApi (DOKS clusters) with centralized
  CLUSTER_ROUTES + DOKS region/size options; base = NEXT_PUBLIC_PLATFORM_URL.
- components/products/ResourceModule.tsx: the factory (list/create/delete,
  copyable masked secret reveal, slug validation, loading/empty/error).
- components/products/ClustersModule.tsx: list + provision DOKS + attach (name
  + kubeconfig) against the platform control plane.
- components/ui/StatusTag.tsx, lib/slug.ts: shared/DRY across both modules.
- registry: swap routes to the working modules and clear 'soon'/'waitlist'
  status so nav badges clear. Base stays a placeholder (no single-resource).
- fix two pre-existing type errors in ComingSoonModule (maxWidth->maxW,
  theme active->blue) so the branch typechecks clean.

kind map: sql->databases, vector->vector, datastore->datastore, kv->kv,
search->search, s3->storage, docdb->docdb.

Verified: npm ci + tsc --noEmit => 0 errors (CI-equivalent flat install).

* console: native product naming + gate Clusters to coming-soon

Native naming (Hanzo brand rule — product name only, no upstream OSS
name in any surface):
- ResourceKind wire kinds align to the cloud /v1 contract exactly:
  databases→sql, storage→s3 (vector/datastore/kv/search/docdb unchanged).
- Strip every upstream name from product descriptions + connectionHints
  (Postgres/Qdrant/ClickHouse/Redis/Meilisearch/Mongo/-compatible → the
  Hanzo product name). Zero forbidden names remain in registry.tsx.

Clusters: register as status:'soon' rendering the coming-soon placeholder
(repo hanzoai/operator). The platform attach/provision endpoints in
platform.ts are unconfirmed (PR not merged), so we don't ship a button to
a non-existent endpoint. ClustersModule.tsx + platform.ts stay in the tree
(unreferenced from the live route) to flip back to enabled in one line once
the platform surface lands. Data products (sql/vector/datastore/kv/search/
s3/docdb) stay ENABLED — their /v1 kinds are real and shipping now.

tsc --noEmit: clean.

---------

Co-authored-by: zeekay <z@zeekay.io>
2026-06-26 13:20:20 -07:00
Hanzo AI 85e68e433e feat(console): unified product hub — catalog, pinnable favorites, account-backed prefs
Turn console2 into the one place to see, enable, and manage every Hanzo
product, with billing → billing.hanzo.ai for all.

- registry: ONE product catalog (categories + module-vs-external + enablement
  status), the single source of truth for nav, overview, and router. Adding a
  product = one CatalogEntry.
- favorites: pin products to the sidebar. Built on a new account-backed
  preferences layer (usePreferences) — customizations persist to the IAM user
  account, so they follow the user across every device/login and product.
  localStorage is only a fast-paint cache.
- shell: Pinned section + categorized catalog; exact-match active (no
  double-highlight); each row opens (in-console route or external tab) + pin toggle.
- overview: product catalog grouped by category with status, pin, and
  Open/Get-started.
- api: AccountApi.updatePreferences → POST /v1/update-preferences (self-scoped).

Typecheck + next build clean.
2026-06-26 11:15:21 -07:00
Hanzo AI 5527bb30dd fix(docker): npm install over npm ci for the @hanzo/gui dep tree
npm ci failed in CI (node:22-alpine npm 10.9) with EUSAGE 'Missing:
react-native-worklets@0.8.3 from lock file' — @hanzo/gui's react-native
optional/platform deps resolve differently across npm versions, so a
lockfile built by one npm is rejected by another's strict ci. npm install
reconciles deterministically for the build platform.
2026-06-25 13:57:42 -07:00
Hanzo AI 1b03d838db deps(console2): declare @zap-proto/web, @zap-proto/zap, superjson
The ZAP-native data layer (src/lib/zap/{client,transport,providers}.ts)
imports @zap-proto/web/client, @zap-proto/zap, and superjson; declare them
so a clean `npm ci` in CI resolves them (the prior build failed: 'Module
not found: @zap-proto/web/client' because the lockfile lacked them).
2026-06-25 13:48:28 -07:00
Hanzo AI 42f591214c feat(providers): cut Providers module to ZAP-native transport
Swap the Providers views from the REST `~/lib/api` to the ZAP-native
`~/lib/zap` (one import line each) — proving the @hanzo/gui + @zap-proto/web
go-forward: same call surface, binary ZAP over WebSocket instead of
JSON-over-HTTP, zero view-component changes.

- src/lib/zap/index.ts: barrel re-exporting ProviderApi (= ProviderApiZap),
  ApiError, Provider — the drop-in twin of ~/lib/api.
- ProviderListView/ProviderEditView: import from ~/lib/zap.
- providers.ts list(): return { rows, total } to match the REST getList
  contract exactly (true drop-in; fixes the list-view shape).

tsc --noEmit (strict) clean. Backs onto cloud's new /zap WS face which
dispatches each call into the same /v1 casibase handlers.
2026-06-25 13:44:26 -07:00
Antje Worring 668f6b8951 ci: env-aware image build (mainnet/testnet/devnet)
workflow_dispatch input 'env' bakes per-env NEXT_PUBLIC_* + tags the image
:dev/:test (mainnet stays :sha-+:latest). Hosts follow svc.env.hanzo.ai. Adds
NEXT_PUBLIC_BILLING_URL build-arg (per-env billing portal).
2026-06-22 03:17:50 -07:00
Antje Worring 7895e104b0 feat(nav): Billing link to the existing billing portal (no rebuild)
Billing is owned by hanzoai/commerce (backend) + hanzoai/billing (portal at
billing.hanzo.ai) — the console must not reimplement payments/balance. Add an
externalLinks registry (orthogonal to product modules) and render it in the
shell; Billing opens config.billingUrl (NEXT_PUBLIC_BILLING_URL, default
https://billing.hanzo.ai) in a new tab.
2026-06-22 02:44:56 -07:00
Antje Worring bfd2a2dd37 fix(auth): treat casibase anonymous-user as logged-out
The backend auto-creates an anonymous-user session for the chat product, so
get-account returns status:ok even with no real sign-in. As an ADMIN console
that made AuthGate show the dashboard for an unauthenticated visitor, while
admin endpoints (get-providers, etc.) rejected the anon session with 'Please
sign in first'. Treat type==='anonymous-user' as null so the console requires
a real IAM sign-in.
2026-06-22 00:39:44 -07:00
Antje Worring cf5d3094ae feat(console2): Models, Applications, Stores, Chat admin surfaces (@hanzo/gui /v1)
Mirror the Providers pattern: product-module + list/edit views on @hanzo/gui +
typed /v1 API modules, registered in the nav. tsc 0 errors, next build green.
2026-06-21 15:58:05 -07:00
Antje Worring 7a3dd59128 fix(ci): ensure public/ exists in image build + skip provenance artifacts
- Dockerfile: mkdir -p public before build (git doesn't track empty public/,
  so the runner COPY /app/public was failing)
- build-image: provenance/sbom false (avoid GitHub artifact-quota upload)
2026-06-21 15:05:32 -07:00
Antje Worring f117a01fac ci: Dockerfile + build-image workflow → ghcr.io/hanzoai/console2
Next.js 15 multi-stage build; NEXT_PUBLIC_* baked at build (same-origin /v1,
IAM client_id=hanzo-cloud to match cloud-api's /v1/signin). Self-hosted ARC
runner, push to ghcr.io/hanzoai/console2.
2026-06-21 15:01:36 -07:00
hanzo-dev 2b4d64c9cf feat: Hanzo Cloud Console (console2) on @hanzo/gui over the unified /v1 backend
Next.js 15 (app router) + @hanzo/gui consumed at runtime via transpilePackages.
Typed /v1 client (Provider/ModelRoute/Application/Store/Chat/Account), Hanzo IAM
OIDC auth, extensible product-module registry, and a full Providers admin surface
(list + view/edit) ported clean from hanzoai/ai onto Gui. BSD-3-Clause.
2026-06-21 14:58:20 -07:00
650 changed files with 26385 additions and 59473 deletions
-4
View File
@@ -1,4 +0,0 @@
[codespell]
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma
ignore-words-list = afterall,vertx
-7
View File
@@ -1,7 +0,0 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
-36
View File
@@ -1,36 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
# Salt for API key hashing
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET_NAME=
S3_REGION=
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
+24
View File
@@ -0,0 +1,24 @@
# Hanzo Cloud Console — environment.
# Copy to .env.local and adjust. All values are public (NEXT_PUBLIC_*) since the
# console is a browser app that talks to the unified /v1 backend with cookies.
# Unified Hanzo Cloud backend (the casibase /v1 API). Default: production.
# Local backend: http://localhost:14000
NEXT_PUBLIC_CLOUD_URL=https://cloud.hanzo.ai
# Hanzo PaaS (platform.hanzo.ai) — DOKS cluster control plane for the Clusters module.
NEXT_PUBLIC_PLATFORM_URL=https://platform.hanzo.ai
# Hanzo IAM (OIDC authority). Canonical issuer is https://hanzo.id — tokens are
# minted with iss=https://hanzo.id, which the cloud /v1 backend validates against.
# iam.hanzo.ai is the legacy zone (iss=https://iam.hanzo.ai) and MUST NOT be used
# by the browser, or sign-in drops on iam.hanzo.ai with an issuer mismatch.
NEXT_PUBLIC_IAM_URL=https://hanzo.id
# IAM application console2 authenticates as. console2 is a front-end of the
# shared Hanzo Cloud /v1 backend, which exchanges the OIDC code and validates the
# token as app `hanzo-cloud` (aud=hanzo-cloud) — so the front-end presents the
# same app/client_id. Must match the cloud-api binding, not a console-only app.
NEXT_PUBLIC_IAM_APP_NAME=hanzo-cloud
NEXT_PUBLIC_IAM_ORG_NAME=hanzo
NEXT_PUBLIC_IAM_CLIENT_ID=hanzo-cloud
-14
View File
@@ -1,14 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
# Next Auth
NEXTAUTH_SECRET="secret"
NEXTAUTH_URL="http://localhost:3000"
# feature flag to enable experimental features locally
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
SALT="salt"
-96
View File
@@ -1,96 +0,0 @@
# More information: https://langfuse.com/docs/deployment/self-host
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
# DATABASE_URL supports pooled connections, but then you need to set DIRECT_URL
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
# DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
# SHADOW_DATABASE_URL=
# Next Auth
# NEXTAUTH_URL does not need to be set when deploying on Vercel
NEXTAUTH_URL="http://localhost:3000"
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
NEXTAUTH_SECRET="secret"
SALT="salt"
# Docker only, optional
# PORT=3000
# HOSTNAME=localhost
# Default project, optional
# LANGFUSE_DEFAULT_PROJECT_ID=
# LANGFUSE_DEFAULT_PROJECT_ROLE=
# Enable experimental features, optional
# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES
# Auth, optional configuration
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_DISABLE_USERNAME_PASSWORD=true
# SSO, each group is optional
# AUTH_GOOGLE_CLIENT_ID=
# AUTH_GOOGLE_CLIENT_SECRET=
# AUTH_GITHUB_CLIENT_ID=
# AUTH_GITHUB_CLIENT_SECRET=
# AUTH_AZURE_AD_CLIENT_ID=
# AUTH_AZURE_AD_CLIENT_SECRET=
# AUTH_AZURE_AD_TENANT_ID=
# Transactional email, optional
# Defines the email address to use as the from address.
# EMAIL_FROM_ADDRESS=
# Defines the connection url for smtp server.
# SMTP_CONNECTION_URL=
# S3 storage, optional, used for exports from the UI
# S3_ENDPOINT=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_BUCKET_NAME=
# S3_REGION=
# Exports are streamed to S3 in pages to avoid memory issues
# The page size can be adjusted if needed to optimize performance
# DB_EXPORT_PAGE_SIZE=1000
### START Langfuse Cloud Config
# Used for Langfuse Cloud deployments
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
# LANGFUSE_TEAM_SLACK_WEBHOOK=
# LANGFUSE_NEW_USER_SIGNUP_WEBHOOK=
# Posthog (optional for analytics of web ui)
# NEXT_PUBLIC_POSTHOG_HOST=
# NEXT_PUBLIC_POSTHOG_KEY=
# Sentry
# NEXT_PUBLIC_SENTRY_DSN=
# NEXT_SENTRY_ORG=
# NEXT_SENTRY_PROJECT=
# SENTRY_AUTH_TOKEN=
# Betterstack
# LANGFUSE_TEAM_BETTERSTACK_TOKEN=
# Demo project that users can use to try the platform
# NEXT_PUBLIC_DEMO_PROJECT_ID=
# Crisp chat
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
### END Langfuse Cloud Config
-47
View File
@@ -1,47 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require("path");
/** @type {import("eslint").Linter.Config} */
const config = {
overrides: [
{
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict-type-checked",
],
files: ["*.ts", "*.tsx"],
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-confusing-void-expression": "off",
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},
plugins: ["@typescript-eslint"],
extends: ["next/core-web-vitals"],
ignorePatterns: ["generated/**/*"],
rules: {
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"react/jsx-key": [
"error",
{
warnOnDuplicates: true,
},
],
},
};
module.exports = config;
-11
View File
@@ -1,11 +0,0 @@
body:
- type: textarea
attributes:
label: Describe the feature or potential improvement
description: Please describe the change as clear and concise as possible. Remember to add context as to why you believe this is needed.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the change here. If your idea is related to any issues or discussions, link them here.
-21
View File
@@ -1,21 +0,0 @@
name: 🐞 Bug Report
description: Create a bug report to help us improve
title: "bug: "
labels: ["🐞❔ unconfirmed bug"]
body:
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
-7
View File
@@ -1,7 +0,0 @@
contact_links:
- name: 💡 Feature Request
url: https://github.com/orgs/langfuse/discussions/new?category=ideas
about: Suggest any ideas you have using our discussion forums.
- name: 🤗 Get Help
url: https://github.com/orgs/langfuse/discussions/new?category=support
about: If you cant get something to work the way you expect, open a question in our discussion forums.
-36
View File
@@ -1,36 +0,0 @@
## What does this PR do?
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes # (issue)
<!-- Please provide a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/
-->
## Type of change
<!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
- [ ] This change requires a documentation update
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist
<!-- Remove bullet points below that don't apply to you -->
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings (`npm run lint`)
- I haven't added tests that prove my fix is effective or that my feature works
- I haven't checked if new and existing unit tests pass locally with my changes
-34
View File
@@ -1,34 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: npm
directory: "/" # Location of package manifests
schedule:
interval: "daily"
rebase-strategy: "disabled" # use dependabot-rebase-stale
commit-message:
prefix: chore
prefix-development: chore
include: scope
ignore:
- dependency-name: "@types/node"
- dependency-name: "@trpc/*"
groups:
sentry:
patterns:
- "@sentry/*"
prisma:
patterns:
- "prisma"
- "@prisma/*"
next:
patterns:
- "eslint-config-next"
- "next"
patches:
update-types:
- "patch"
+51
View File
@@ -0,0 +1,51 @@
name: Build Docker Image
# Builds + pushes ghcr.io/hanzoai/console2 on the self-hosted ARC runner. ONE
# brand-agnostic image serves every brand: console2 resolves the brand at RUNTIME
# from the request hostname (console.hanzo.ai → hanzo, console.lux.cloud → lux,
# console.zoo.cloud → zoo; src/config/index.ts), and /v1 is same-origin per host.
# So NO NEXT_PUBLIC_* are baked — baking them would pin the image to one brand.
# Tags: SEMVER ONLY (no sha, no :latest) — a `v*` git tag publishes that exact
# version; a main push publishes `v<package.json version>` (bump to release).
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
concurrency:
group: docker-image-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: [hanzo-build-linux-amd64]
steps:
- uses: actions/checkout@v4
- name: resolve semver tag
id: ver
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
else
echo "tag=v$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
fi
- uses: docker/setup-buildx-action@v3
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
provenance: false
sbom: false
tags: |
ghcr.io/hanzoai/console2:${{ steps.ver.outputs.tag }}
-69
View File
@@ -1,69 +0,0 @@
name: CI
on:
pull_request:
branches: ["*"]
push:
branches: ["main", "cloud"]
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build-lint:
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
SHADOW_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Start containers
run: docker-compose -f "docker-compose.yml" up -d --build
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
- name: Setup Node 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install deps (with cache)
run: pnpm install
# Normally, this would be done as part of the turbo pipeline - however since the Expo app doesn't depend on `@acme/db` it doesn't care.
# TODO: Free for all to find a better solution here.
- name: Deploy db
run: pnpm turbo db:deploy
- name: Generate Prisma Client
run: pnpm turbo db:generate
- name: Build, lint and type-check
run: pnpm turbo build lint type-check
env:
SKIP_ENV_VALIDATION: true
# FIXME: Add this back once we have an Expo SDK supporting React 18.2
# - name: Check workspaces
# run: pnpm manypkg check
-22
View File
@@ -1,22 +0,0 @@
---
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Codespell
uses: codespell-project/actions-codespell@v2
@@ -1,18 +0,0 @@
name: Rebase Dependabot stale PRs
on:
push:
branches:
- main
workflow_dispatch:
jobs:
rebase-dependabot:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- name: "Rebase open Dependabot PR"
uses: orange-buffalo/dependabot-auto-rebase@v1
with:
api-token: ${{ secrets.GH_ACCESS_TOKEN }}
repository: ${{ github.repository }}
-49
View File
@@ -1,49 +0,0 @@
name: License Compliance Check
on:
workflow_dispatch:
push:
branches:
- "main"
merge_group:
pull_request:
branches:
- "main"
jobs:
license_check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 18
- name: Install license-checker
run: npm install -g license-checker
- name: Install yui-lint
run: npm install yui-lint
- name: Generate license-checker CSV file
run: license-checker --production --csv > npm-license-checker.csv
- name: Check license-checker CSV file without headers
id: license_check_report
uses: pilosus/action-pip-license-checker@v2
with:
external: "npm-license-checker.csv"
external-format: "csv"
external-options: "{:skip-header true}"
fail: "WeakCopyleft,StrongCopyleft,NetworkCopyleft"
fails-only: true
totals: true
verbose: 1
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Echo error
if: failure()
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
- name: Delete license-checker CSV file
run: rm npm-license-checker.csv
-187
View File
@@ -1,187 +0,0 @@
name: CI/CD
on:
workflow_dispatch:
push:
branches:
- "main"
tags:
- "v*"
merge_group:
pull_request:
branches:
- "main"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
cache: "npm"
- name: install dependencies
run: |
npm ci
- name: Load default env
run: |
cp .env.dev.example .env
- name: lint
run: npm run lint
test-docker-build:
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: Checkout
uses: actions/checkout@v3
- name: Build Docker image
uses: docker/build-push-action@v4
with:
context: .
push: false
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: install dependencies
run: |
npm ci
- name: Load default env
run: |
cp .env.dev.example .env
- name: Run, migrate, seed DB
run: |
docker-compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
npx --yes prisma migrate reset --force --skip-generate
- name: Build
run: npm run build
- name: Start Langfuse
run: (npm start&)
- name: run tests
run: npm run test
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
cache: "npm"
- name: install dependencies
run: |
npm ci
- name: Load default env
run: |
cp .env.dev.example .env
- name: Run, migrate, seed DB
run: |
docker-compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
npx --yes prisma migrate reset --force --skip-generate
- name: Build
run: npm run build
- name: Install playwright
run: npx playwright install
- name: Run e2e tests
run: npm run test:e2e
all-ci-passed:
# This allows us to have a branch protection rule for tests and deploys with matrix
runs-on: ubuntu-latest
needs: [lint, tests, e2e-tests, test-docker-build]
if: always()
steps:
- name: Successful deploy
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failing deploy
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
push-docker-image:
needs: all-ci-passed
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
environment: "protected branches"
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
packages: write
contents: read
steps:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: Checkout
uses: actions/checkout@v3
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-19
View File
@@ -1,19 +0,0 @@
on:
workflow_dispatch:
push:
# Pattern matched against refs/tags
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # Semantic version tags
jobs:
release:
runs-on: ubuntu-latest
environment: "protected branches"
steps:
- uses: actions/checkout@v4
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Push to production
run: git push origin +main:production
+18 -49
View File
@@ -1,55 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
.next/
out/
dist/
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
# build artifacts
*.tsbuildinfo
next-env.d.ts
# production
/build
# test artifacts (the suites under test/ ARE committed; only outputs are ignored)
/test-results/
/playwright-report/
/blob-report/
/coverage/
/.playwright/
# idea
.idea
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env.local
# env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
/generated/typescript-server
# openapi spec that is copied during build
/public/openapi*.yml
# vscode
.devcontainer
# editor / os
.DS_Store
.vscode/
.idea/
*.log
-1
View File
@@ -1 +0,0 @@
v20
-10
View File
@@ -1,10 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"unifiedjs.vscode-mdx",
"yoavbls.pretty-ts-errors",
"Prisma.prisma"
]
}
-28
View File
@@ -1,28 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}
-33
View File
@@ -1,33 +0,0 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.rulers": [100],
"editor.tabSize": 2,
"eslint.validate": [
"javascript",
"javascriptreact",
"astro",
"typescript",
"typescriptreact"
],
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
"typescript.tsdk": "node_modules/typescript/lib",
"prettier.documentSelectors": [
"**/*.{cjs,mjs,ts,tsx,astro,md,mdx,json,yaml,yml}"
],
"mdx.experimentalLanguageServer": true,
"[astro]": {
"editor.defaultFormatter": "astro-build.astro-vscode"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"docwriter.style": "JSDoc",
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"eslint.lintTask.enable": true
}
+35
View File
@@ -0,0 +1,35 @@
# AGENTS — console2
Read [LLM.md](./LLM.md) first; it is the canonical design doc. Highlights for
agents working here:
- **Stack:** Next.js 15 (app router) + @hanzo/gui (npm), consumed at runtime via
`transpilePackages` (the `@hanzogui/next-plugin` is broken on npm). Next 15
(not 14) because @hanzo/gui needs React 19. Pin patch versions; never lazily
major-bump.
- **Gui style props:** the v5 config is `onlyShorthandStyleProps` — use
shorthands (`p`, `px`, `bg`, `items`, `justify`, `self`, `rounded`, `minH`),
never longhands (`padding`, `backgroundColor`, …). Keep `tsc` clean.
- **One way:** all backend calls go through `src/lib/api` (never raw `fetch`);
all selects/inputs through `src/components/ui/Field.tsx`; all nav/routing
through the registry in `src/lib/products`.
- **Extensibility:** add a cloud product by appending a `ProductModule` to
`src/lib/products/registry.tsx` and writing its module component — do not add
per-product routes or touch the shell.
- **Auth:** Hanzo IAM (OIDC) via `@hanzo/iam-js-sdk`; session cookie minted by
the backend at `/v1/signin`. Never store credentials client-side.
- **Boundaries:** frontend only. No DB. No Docker builds locally (CI/CD builds
images). No secrets in the repo — config is `NEXT_PUBLIC_*` only.
- **Verify:** `npm run typecheck` and `npm run build` must pass. Show output.
- **Tests (real, committed under `test/`):**
- `npm run test:unit` — vitest, pure client logic + catalog/data-integrity
(routing, registry, config, the `/v1` client envelope, domain `logic.ts`).
Heavy GUI deps are aliased to hermetic stubs (`test/stubs/`) so the registry
graph imports without rendering Tamagui in Node.
- `npm run test:e2e` — Playwright against the real Next server (builds + serves
via `webServer`). HERMETIC: the `/v1` + `/paas` backend is mocked with route
interception (`test/e2e/fixtures.ts`) — tests NEVER touch real prod data.
Fixtures: `ACCOUNTS.admin/member/anonymous`, `backend.account()/envelope()/
rest()/error()/paas()`, `baseline()`, `landAs()`, `trackConsoleErrors()`.
- `npm run test:all` — both. E2E scopes assertions with the `nav-sidebar`,
`page-content`, and `pinned-section` testIDs on the shell.
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
-218
View File
@@ -1,218 +0,0 @@
# Contributing to Langfuse
First off, thanks for taking the time to contribute! ❤️
The best ways to contribute to Langfuse:
- Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
- Create and comment on [Issues](https://github.com/langfuse/langfuse/issues)
- Open a PR.
We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
We gratefully welcome improvements to documentation ([docs repo](https://github.com/langfuse/langfuse-docs)), the core application (this repo) and the SDKs ([Python](https://github.com/langfuse/langfuse-python), [JS](https://github.com/langfuse/langfuse-js)).
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
> And if you like the project, but just don't have time to contribute code, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project;
> - Tweet about it;
> - Refer to this project in your project's readme;
> - Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas);
> - Create and comment on [Issues](https://github.com/langfuse/langfuse/issues);
> - Mention the project at local meetups and tell your friends/colleagues.
## Making a change
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone. Large changes that were not discussed in an issue may be rejected.
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Project Overview
### Technologies we use
- Application (this repository)
- NextJS 14, pages router
- NextAuth.js / Auth.js
- tRPC: Frontend APIs
- Prisma ORM
- Zod
- Tailwind CSS
- shadcn/ui tailwind components (using Radix and tanstack)
- Fern: generate OpenAPI spec and Pydantic models
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
- openapi-typescript to generated types based on OpenAPI spec
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
- Pydantic for input validation, models generated by fern
### Architecture Overview
```mermaid
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python SDK"]
OAI["OpenAI drop-in replacement"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
DB[Postgres Database]
subgraph s1["Application (langfuse/langfuse)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
Prisma[Prisma ORM]
H --> G
H --> I
G --> I
G --- Prisma
API --- Prisma
I --- Prisma
end
Prisma --- DB
JS --- API
Python --- API
```
### Database Overview
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
Full database schema: [prisma/schema.prisma](prisma/schema.prisma)
<img src="./prisma/database.svg">
### Infrastructure & Network Overview
```mermaid
flowchart LR
Browser ---|Web UI & TRPC API| App
Integrations/SDKs ---|Public HTTP API| App
subgraph i1["Application Network"]
App["Langfuse Application (Docker or Serverless)"]
end
subgraph i2["Database Network"]
DB["Postgres Database"]
end
App --- DB
```
## Development Setup
Requirements
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
- Docker to run the database locally
**Steps**
1. Fork the the repository and clone it locally
2. Install dependencies
```bash
npm install
```
3. Run the development database
```bash
docker-compose -f docker-compose.dev.yml up -d
```
4. Create an env file
```bash
cp .env.dev.example .env
```
5. Run the migrations
```bash
npm run db:migrate
# Optional: seed the database
# npm run db:seed
# npm run db:seed:examples
```
6. Start the development server
```bash
npm run dev
```
> [!NOTE]
> If you frequently switch branches, use `npm run dx` instead of `npm run dev`. This command will install dependencies, reset the database (wipe and apply all migrations), and run the database seeder with example data before starting the development server.
## Commit messages
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
## Test the public API
The API is tested using Jest. With the development server running, you can run the tests with:
Run all
```bash
npm run test
```
Run interactively in watch mode
```bash
npm run test:watch
```
These tests are also run in CI.
## CI/CD
We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pipeline.yml`](.github/workflows/pipeline.yml)
CI on `main` and `pull_request`
- Check Linting
- E2E test of API using Jest
- E2E tests of UI using Playwright
CD on `main`
- Publish Docker image to GitHub Packages if CI passes. Done on every push to `main` branch. Only released versions are tagged with `latest`.
## Staging environment
We run a staging environment at [https://staging.langfuse.com](https://staging.langfuse.com) that is automatically deployed on every push to `main` branch.
The same environment is also used for preview deployments of pull requests. Limitations:
- SSO is not available as dynamic domains are not supported by most SSO providers.
- When making changes to the database, migrations to the staging database need to be applied manually by a maintainer. If you want to interactively test database changes in the staging environment, please reach out.
You can use the staging environment end-to-end with the Langfuse integrations or SDKs (host: `https://staging.langfuse.com`). However, please note that the staging environment is not intended for production use and may be reset at any time.
## Production environment
When a new release is tagged on the `main` branch (excluding prereleases), it triggers a production deployment. The deployment process consists of two steps:
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
+32 -73
View File
@@ -1,81 +1,40 @@
# Base image
FROM node:20-alpine AS base
# It's important to update the index before installing packages to ensure you're getting the latest versions.
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
FROM base AS deps
# console2 — Hanzo Cloud Console (Next.js 15 + @hanzo/gui). BSD-3-Clause.
# NEXT_PUBLIC_* are inlined at build time (browser config), so they are build args.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
# npm install (not ci): @hanzo/gui pulls a react-native dep tree whose
# platform/optional packages (e.g. react-native-worklets) resolve differently
# across npm versions, so a lockfile generated by one npm fails `npm ci` under
# another (EUSAGE "Missing: react-native-worklets@... from lock file"). install
# reconciles the tree deterministically for the build platform.
RUN npm install --no-audit --no-fund
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# remove middleware.ts if it exists - not needed in self-hosted environments
RUN rm -f ./src/middleware.ts
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
# Disable validation of environment variables during build
ENV DOCKER_BUILD 1
# Generate prisma client
RUN npx prisma generate
# Build the application
# public/ may be empty (git doesn't track empty dirs) — ensure it exists for the runner COPY.
RUN mkdir -p public
# ONE brand-agnostic image: brand (IAM org/issuer/app + wordmark) is resolved at
# RUNTIME from the request hostname (src/config/index.ts), and /v1 is same-origin
# per host. Baking NEXT_PUBLIC_* here would inline a single brand and break that —
# so nothing brand-specific is baked.
# Next 15 + @hanzo/gui (large RN dep tree) overflows Node's default heap on the
# build node → OOMKill (exit 137). Cap the heap generously, as every other Hanzo
# Next build does (chat uses 4096).
ENV NEXT_TELEMETRY_DISABLED=1 NODE_OPTIONS=--max-old-space-size=6144
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
RUN apk add --no-cache dumb-init
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
# Needed to re-enable validation of environment variables during runtime
ENV DOCKER_BUILD 0
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN npm install -g --no-package-lock --no-save prisma
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
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
COPY --chown=nextjs:nodejs entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
USER nextjs
# Default port to 3000
ENV PORT 3000
# CMD ["node", "server.js"]
CMD ["dumb-init", "--", "./entrypoint.sh"]
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=4000
RUN addgroup -S app && adduser -S app -G app
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/next.config.mjs ./next.config.mjs
USER app
EXPOSE 4000
CMD ["npm", "run", "start"]
+37 -20
View File
@@ -1,25 +1,42 @@
Copyright (c) 2023 Finto Technologies GmbH
BSD 3-Clause License
Portions of this software are licensed as follows:
Copyright (c) 2026-present, Hanzo AI, Inc.
* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
* All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
Portions of this software are derived from upstream code originally licensed under
the MIT License, with the following copyright notices retained per its terms:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Copyright (c) 2020 Nate Wienert
Copyright (c) 2015-present, Nicolas Gallagher.
Copyright (c) 2015-present, Facebook, Inc.
Copyright (c) 2021 Radix
Copyright (c) 2017 Carmelo Pullara
Copyright (c) 2018 Framer B.V.
Copyright (c) 2022 WorkOS
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
All rights reserved.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+314
View File
@@ -0,0 +1,314 @@
# console2 — Hanzo Cloud Console
Unified admin console for **Hanzo Cloud** and all cloud products. Our code,
BSD-3-Clause, built on **@hanzo/gui** (the Tamagui-based cross-platform UI).
NOT a Langfuse fork, NOT casibase — it is a clean client over the unified `/v1`
backend (`hanzoai/cloud`, the casibase API at https://cloud.hanzo.ai/v1/*).
## Base: Next.js 15 (app router) + @hanzo/gui
The @hanzo/gui `expo-router` template was evaluated first and **rejected for a
standalone repo**: it declares `workspace:*` dependencies (`hanzogui`,
`@hanzogui/config`, `@hanzogui/babel-plugin`, …) that only resolve inside the gui
bun monorepo — `npm install` of a copy fails with
`EUNSUPPORTEDPROTOCOL "workspace:"`. It is also native-first with no real
typecheck (`"test": "true"`), a poor fit for a data-heavy web admin.
So the base is **Next.js + @hanzo/gui (npm)**. Gui is consumed at **runtime**:
Next's built-in `transpilePackages` transpiles the Gui ESM packages (discovered
from `node_modules/@hanzogui`, not hardcoded) and `GuiProvider` injects CSS at
runtime. Gui is designed to work this way — the optimizing compiler is an
optimization, not a requirement.
The canonical `@hanzogui/next-plugin@7.3.0` is **broken on npm**: it depends on
`hanzogui-loader@7.3.0` (unpublished — only `2.x`/`102.x` fork tags exist), and
that fork renames the export the plugin imports (`GuiPlugin``HanzoguiPlugin`).
Pinning the fork via overrides surfaces the rename at build time. So the plugin
is unusable standalone; `transpilePackages` is the clean, supported path.
**v5 config uses `onlyShorthandStyleProps`** — components use Gui shorthand style
props (`p`, `px`, `bg`, `items`, `justify`, `self`, `rounded`, `minH`, …), not
longhands. With shorthands, `tsc --noEmit` (strict) passes clean and the build
type-checks with no suppression.
**Next 15, not 14:** @hanzo/gui requires `react>=19`. Next 14 ships React 18 and
cannot run React 19 (App Router server components are version-locked to the
bundled React). Next 15.5.x is the current stable that natively supports React
19 — so 15 is the correct, non-degrading choice. The task's "Next 14" is
impossible without downgrading Gui or breaking the peer tree.
## Layout
```
app/ Next.js app router
layout.tsx html shell, dark default, mounts <Provider>
globals.css base resets (Gui CSS injected via plugin)
signin/page.tsx sign-in (delegates to IAM)
auth/callback/page.tsx OIDC callback -> /v1/signin -> session
(dashboard)/
layout.tsx AuthGate + DashboardShell
page.tsx product overview cards
[...slug]/page.tsx catch-all: resolves a module+route from the registry
src/
config/index.ts single env reader (NEXT_PUBLIC_*), branding
lib/
api/ typed /v1 client (ours)
client.ts core request: cookies, envelope unwrap, ApiError
types.ts Provider, ModelRoute, Application, Store, Chat, Account
providers|model-routes|applications|stores|chats|account.ts
index.ts barrel
auth/
iam.ts @hanzo/iam-js-sdk wrapper (browser-only), getSigninUrl
session.tsx SessionProvider/useSession (account, signIn, signOut)
products/
registry.tsx ProductModule[] — the extensibility backbone
match.ts slug -> {module, route, params}
components/
Provider.tsx GuiProvider + next-theme + SessionProvider (dark)
DashboardShell.tsx sidebar (from registry) + topbar (adapts dashboard-shell recipe)
AuthGate.tsx gate authenticated routes
SignInForm.tsx adapts sign-in-form recipe; IAM redirect
ui/ PageHeader, DataTable, Field*
products/
ProvidersModule.tsx FULL surface: list + view/edit
providers/ logic.ts (pure cascade/visibility), List/Edit views
ModelsModule.tsx routes list <-> new/edit
models/ logic.ts (newModelRoute), ModelRoute List/Edit views
ApplicationsModule.tsx routes list <-> edit
applications/ logic.ts (newApplication), List/Edit (deploy/undeploy)
StoresModule.tsx routes list <-> edit
stores/ logic.ts (newStore), List (refresh-vectors)/Edit views
ChatModule.tsx routes list <-> read-only chat view
chat/ ChatListView + ChatView (message thread)
```
Each product module mirrors Providers: a router module (`<X>Module.tsx`), a
list view + an edit/view, and a pure `logic.ts` (new-record templates / option
lists). Every module declares a `''` (list) and `:name` (edit/view) route in the
registry; Models also handles `:name === 'new'` for create (model routes are
keyed by `owner/modelName`, so modelName is form-entered, not generated).
## /v1 backend client
One `request()` in `lib/api/client.ts`: always `credentials: 'include'` (the
backend sets a session cookie at `/v1/signin`), forwards `Accept-Language`,
unwraps the casibase `{ status, msg, data, data2 }` envelope, throws typed
`ApiError` (401/403 carry status). Base URL = `config.cloudUrl` (default
`https://cloud.hanzo.ai`, override `NEXT_PUBLIC_CLOUD_URL`).
Endpoint surface ported from `hanzoai/ai` `web/src/backend/*.js`
(see `docs/endpoints.md`):
- **ProviderApi** — get-global-providers, get-providers, get-provider,
add/update/delete-provider, refresh-mcp-tools
- **ModelRouteApi** — get(-model-routes|-route), add/update/delete-model-route
- **ApplicationApi** — get(-applications|-application), add/update/delete,
deploy/undeploy-application
- **StoreApi** — get-global-stores, get-stores, get-store, get-store-names,
add/update/delete-store, refresh-store-vectors
- **ChatApi** — get-global-chats, get-chats, get-chat, add/update/delete-chat
- **AccountApi** — get-account, signin, signout
## Auth (Hanzo IAM)
`@hanzo/iam-js-sdk` against **`hanzo.id`** — the canonical OIDC issuer
(`iss=https://hanzo.id`), the one the cloud `/v1` backend validates. `getSigninUrl()`
builds the authorize URL (`https://hanzo.id/login/oauth/authorize?...redirect_uri=
<origin>/auth/callback`). IAM returns `?code&state`; the callback posts them to
`/v1/signin`, which the cloud backend exchanges and mints the session cookie;
`useSession` then loads `/v1/get-account`.
App/client is **`hanzo-cloud`**, org `hanzo` — NOT a console-specific app. console2
is a front-end OF the shared cloud `/v1` backend, which exchanges the code and
validates the token as app `hanzo-cloud` (`aud=hanzo-cloud`), so the browser MUST
present the same `client_id`. (The `hanzo-cloud` IAM app already whitelists
`https://console2.hanzo.ai/auth/callback`.)
**Build-time gotcha (the 2026-06 sign-in bug):** every `NEXT_PUBLIC_IAM_*` is
inlined at BUILD time (browser config), so the *image* — not runtime env — decides
the issuer. The mainnet image MUST bake `NEXT_PUBLIC_IAM_URL=https://hanzo.id`.
Baking `iam.hanzo.ai` (the legacy zone, `iss=https://iam.hanzo.ai`) dropped the user
on iam.hanzo.ai with an issuer mismatch. Fixed in `src/config/index.ts` (default),
`.env.example`, the `Dockerfile` ARG default, and the mainnet `iam_url` build-arg in
`.github/workflows/build-image.yml`.
## Product-module registry (extensibility)
`lib/products/registry.tsx` is the single source of nav + routing truth. Each
cloud product is a `ProductModule { id, label, icon, description, routes }`. The
sidebar, overview, and the catch-all route all render from it. **Adding a cloud
product = appending one entry + its module component(s); no shell or route
edits.** A module owns its routes and components and knows nothing about
siblings (orthogonal).
## Dev
```bash
npm install
cp .env.example .env.local # set NEXT_PUBLIC_IAM_CLIENT_ID for live auth
npm run typecheck # tsc --noEmit (strict) — clean
npm run build # next build (type-checks; Gui CSS injected at runtime)
npm run dev # http://localhost:4000
```
Data layer is the unified `/v1` backend — this repo is frontend only. Do NOT
add Postgres/Mongo/etc. Do NOT build Docker images locally (CI/CD does that).
## Testing (committed under `test/`)
Two layers, orthogonal: vitest for pure logic + data-integrity, Playwright for
real rendered UI + interaction. Run:
```bash
npm run test:unit # vitest (jsdom) — fast, hermetic, no server
npm run test:e2e # playwright — builds + serves the real app, mocks /v1
npm run test:all # both
```
- **Unit (`test/unit/`, vitest 3, `vitest.config.ts`)** — 102 tests over the pure
surface: `matchRoute`, the catalog/registry invariants (unique ids, no dead
routes, no empty category, admin-visibility), `brandFromHost`/`resolveConfig`,
the `/v1` client (envelope unwrap, `ApiError`, REST layer), `AccountApi`
(anonymous == logged-out), honest-state mapping, and the provider/model/store/
app domain `logic.ts`. The heavy GUI deps (`@hanzo/gui`, `@hanzogui/*`, icons,
`ethers`, `@zap-proto/*`, the IAM SDK) are aliased to hermetic stubs in
`test/stubs/` so the registry module graph imports without rendering Tamagui in
Node. Real rendering is the E2E layer's job.
- **E2E (`test/e2e/`, `@playwright/test`, `playwright.config.ts`)** — 98 tests
exercising real clicks/forms/navigation across auth, the catalog home, the
sidebar, every enabled module (+ honest empty/403/404/503 states), all admin
flows (IAM tabs, Audit, Secrets/KMS, Clusters, Kubernetes, Settings, API Keys),
negative authz, forms, mobile+desktop viewports, and clean-console checks.
HERMETIC + SAFE: the `/v1` envelope API, the plain-REST provisioning kinds, and
the `/paas` proxy are all mocked with route interception in
`test/e2e/fixtures.ts` (`backend.account()/envelope()/rest()/error()/paas()`,
`baseline()`, `landAs()`, `trackConsoleErrors()`) — the suite NEVER touches
real prod data. The shell exposes `nav-sidebar` / `page-content` /
`pinned-section` testIDs purely to scope E2E assertions.
Bugs found + fixed via TDD while writing the suite:
- **Dead default pin** — `favorites.tsx` `DEFAULT_PINNED` named `billing`, which
is not a catalog id (the billing surface id is `cost`); the shell silently
drops unknown pins, so new users lost a pin. Fixed to `['chat','cost']`.
- **Admin nav leak (least privilege)** — the sidebar and catalog home rendered
admin-only entries (IAM/KMS/Secrets/Audit/Clusters/Kubernetes) to every user.
Added `visibleCatalog(isAdmin)` (registry), wired into the shell + home, so
non-admins never see admin surfaces. Server-side 403 remains the authority
(defense in depth).
- **Host→brand gap** — `brandFromHost` mapped every brand's `.id` host except
`pars.id` (asymmetric). Added it.
## Cloud console — 10-category CLOUD AXIS + embedded PaaS (feat/cloud-taxonomy-10cat)
The catalog (`src/lib/products/registry.tsx`) is reorganized from 6 ad-hoc
categories to the canonical **10-category cloud axis** (the same taxonomy as the
hanzo.ai product surface, `/tmp/hanzo-cloud-taxonomy.md`), so console2 reads like
a cloud console (GCP/AWS) — resources grouped by cloud primitive, two rows of
five:
```
AI Compute Data Network Security
Dev Deploy Observe Chain Apps
```
**Three entry kinds, zero dead links, zero fakes** (`CatalogEntry.kind` +
`status`):
- `module` — in-console admin surface (Providers/Models/Chat/Stores=Vector/
Applications, + the embedded PaaS).
- `external`— a REAL Hanzo product on its own domain (Inference→api.hanzo.ai,
Search→search.hanzo.ai, Bot→hanzo.bot, IAM→iam, KMS→kms, Observe/Traces/
Dashboards→console.hanzo.ai, Analytics, Cost→billing, Object Storage→s3,
Edge, Flow, Sign, Crawl, Studio).
- `soon` — a real cloud primitive without a UI yet (GPUs, VPC, HSM, Settlement,
…). Renders an HONEST in-console "coming soon" overview (`ComingSoon.tsx`,
resolved by id from the path) that points at the API/CLI — **never a 404 and
never a fabricated product card**. A `soon` entry is a `module` under the hood
(single route → `ComingSoon`), so routing is unchanged.
The nav shell, catalog home, favorites, and router still render from the one
`catalog` list. `status: 'soon'` shows a "Coming soon" badge + affordance.
### Job 3 — PaaS embedded natively under Deploy (NOT an iframe)
`PlatformModule.tsx` is the embedded PaaS, wired to the REAL platform.hanzo.ai
control plane. The browser calls console2's OWN origin under `/paas/*`; the
server route `app/paas/[...path]/route.ts` forwards to `platform.hanzo.ai/v1/*`
with the service token from **server-only** env `PAAS_SERVICE_TOKEN` (sourced via
KMS — never `NEXT_PUBLIC_`, never in the browser bundle, no CORS). It lists real
apps across clusters with **declared vs running tag + drift** and a real
health-gated **redeploy** (`POST /v1/apps/<id>/redeploy`). The six Deploy
sub-pages (Projects/Environments/Builds/Registry/Releases/Pipelines) are tabs
over the same real inventory. States are honest: loading, **not-configured (501
when `PAAS_SERVICE_TOKEN` is unset)**, error, empty — it never invents rows.
To light up real data in prod: add `PAAS_SERVICE_TOKEN` (+ optional
`PLATFORM_URL`) to the console2 deployment env via a KMSSecret.
### Job 4 — no fake/placeholder/stub data
The catalog is honest by construction (every leaf → real module, real product
domain, or honest `soon` overview). The PaaS embed shows only real control-plane
data with honest empty/not-configured states. No lorem stats, no demo projects,
no placeholder cards.
Build: arcd self-hosted CI (`.github/workflows/build-image.yml`, push to `main`
`ghcr.io/hanzoai/console2:v<package.json version>`, SEMVER only). The
`hanzo-build-linux-amd64` ARC runner pool is the builder (online; not GHA-hosted).
Deploy: console2 IS an operator `Service` CR now (`hanzo.ai/v1`, `hsvc console2`,
ns `hanzo`) — declared in `universe/infra/k8s/operator/crs/console2-v1.yaml`.
Bump `spec.image.tag`, `kubectl apply`, the operator reconciles. Verify live with
headless Playwright on console2.hanzo.ai.
## Live verification + backend wiring (v0.1.8)
> v0.1.7 was a parallel CTO branch (`fix/paas-live-data`) that wired only
> Clusters/Kubernetes/Status to the platform `/v1` surface; it is integrated here
> (`-s ours`) and superseded — v0.1.8 is the cumulative release with the full set
> below.
Every embedded module was Playwright-verified live against the real `/v1` backend
(authenticated hanzo-org admin). The backend topology console2 actually talks to:
the same-origin `/v1` ingress routes to **cloud-api** directly (NOT the full
api.hanzo.ai gateway), and `/paas/*` is console2's own server route → platform.
Findings + fixes (all in console2; honest states everywhere, no fakes):
- **X-Org-Id (the big one).** The provisioning sub-service (vector/sql/kv/s3/
datastore/docdb/search) requires an `X-Org-Id` header and 403s `"X-Org-Id
required"` without it — cloud-api on the direct path does NOT inject it from the
session. Fix: `lib/api/client.ts` now stamps `X-Org-Id: config.iamOrgName`
(brand org, the user's own) on every cloud call (`baseHeaders`). All 7 data
modules now return real data / honest empty `[]`.
- **PaaS token was wrong.** The CR wired `PAAS_SERVICE_TOKEN` to
`hanzo-paas/MASTERTOKEN` (`hanzo-master-token`), which platform.hanzo.ai
**rejects (401)**. The correct token is in secret **`paas-console-token`** key
`PAAS_SERVICE_TOKEN` (== `platform-service-token`). CR repointed there.
- **Platform contract was wrong.** The real platform serves `GET /v1/apps` (the
apps inventory: declared/running/latest tag + drift + health + cluster +
namespace, ~100 services) and `GET|POST /v1/org/{org}/cluster` — NOT
`/v1/clusters` and NOT any `/k8s/{kind}` passthrough (those 401/404). `lib/api/
platform.ts` reworked to `PlatformApi.apps()` + org-scoped `listClusters`/
`provisionCluster`; dead `KubernetesApi`/`CLUSTER_ROUTES` removed.
- **Status** now reads `/v1/apps` → REAL health board (Services/Healthy/Clusters).
- **Kubernetes** now reads `/v1/apps` → REAL workloads per cluster (picker from
the clusters that actually appear).
- **Clusters** lists real dedicated DOKS via `/v1/org/{org}/cluster` (honest
empty; provision form wired to the real endpoint). Attach-by-kubeconfig dropped
(no backend).
- `interpretPlatformError` maps upstream 401/403 → honest "not configured".
- **Bot** `/v1/bot/health` 404s on cloud-api (bot-gateway runs behind
api.hanzo.ai/hanzo.bot, not this host) → honest "not routed on this host" state
(was a red error).
- **Wallet** cloud-credit `/v1/billing/balance` 404s here (billing ships
separately) → honest "not available on this deployment" (was a scary error).
HUSD balance/top-up already honest "coming" (token unconfigured).
- **Providers was broken** — `ProviderListView`/`ProviderEditView` imported the
ZAP twin (`~/lib/zap`), but the cloud `/zap` WS face is NOT served (the edge
returns SPA HTML, 200 not a WS upgrade — documented in `lib/zap/client.ts`), so
the module showed "Failed to load providers". Switched both back to the working
REST `~/lib/api` (identical surface). The ZAP twin stays as the proof-of-pattern
until `/zap` is bound. Providers now shows real/empty over REST like every module.
- Already-correct honest states (unchanged): IAM/Audit + KMS/Secrets (`/v1/iam`,
`/v1/kms` 404 → "not available on this deployment"); Observability (`/v1/o11y`
503 → "runtime not initialized"). Plans/Embeddings show real data; Models/
Providers/Applications/Chat honest-empty.
`StatusTag` now also understands platform health verdicts (green/yellow/red).
+33 -167
View File
@@ -1,183 +1,49 @@
<div align="center">
<a href="https://langfuse.com">
<h1>🪢 Langfuse</h1>
</a>
<div>
<h3> <a href="https://cloud.langfuse.com">
<strong>Sign up</strong>
</a> ·
<a href="https://langfuse.com/docs/deployment/self-host">
<strong>Self Host Langfuse</strong>
</a> ·
<a href="https://langfuse.com/demo">
<strong>Demo Project (live data)</strong>
</a>
</h3>
<h3>
Langfuse is the open source LLM engineering platform.
</h3>
<div>
Debug, analyze and iterate - together
</div>
</br>
<div>
<a href="https://langfuse.com/docs">
<strong>Docs</strong>
</a> ·
<a href="https://langfuse.com/issue">
<strong>Report Bug</strong>
</a> ·
<a href="https://langfuse.com/idea">
<strong>Feature Request</strong>
</a> ·
<a href="https://langfuse.com/changelog">
<strong>Changelog</strong>
</a> ·
<a href="https://langfuse.com/discord">
<strong>Discord</strong>
</a>
</div>
</br>
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse" alt="langfuse npm package"></a>
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/v/langfuse.svg?style=flat-square&label=pypi+langfuse" alt="langfuse Python package on PyPi"></a>
</div>
</div>
</br>
# Hanzo Cloud Console
## Overview
Unified admin console for **Hanzo Cloud** and all Hanzo cloud products. Built on
[@hanzo/gui](https://gui.hanzo.ai) (cross-platform UI) over the unified `/v1`
backend (`hanzoai/cloud`). Dark theme, OIDC sign-in via Hanzo IAM.
### Develop
Manages: **Providers · Models · Applications · Stores · Chat** — with an
extensible product-module registry so every cloud product can be added as a
module.
- **Observability:** Instrument your app and start ingesting traces to Langfuse ([Quickstart](https://langfuse.com/docs/get-started), [Integrations](https://langfuse.com/docs/integrations) [Tracing](https://langfuse.com/docs/tracing))
- **Langfuse UI:** Inspect and debug complex logs ([Demo](https://langfuse.com/docs/demo), [Tracing](https://langfuse.com/docs/tracing))
- **Prompts:** Manage, version and deploy prompts from within Langfuse ([Prompt Management](https://langfuse.com/docs/prompts))
### Monitor
- **Analytics:** Track metrics (cost, latency, quality) and gain insights from dashboards & data exports ([Analytics](https://langfuse.com/docs/analytics))
- **Evals:** Collect and calculate scores for your LLM completions ([Scores & Evaluations](https://langfuse.com/docs/scores))
- Run model-based evaluations ([Model-based evaluations](https://langfuse.com/docs/scores/model-based-evals))
- Collect user feedback ([User Feedback](https://langfuse.com/docs/scores/user-feedback))
- Manually score observations in Langfuse ([Manual Scores](https://langfuse.com/docs/scores/manually))
### Test
- **Experiments:** Track and test app behaviour before deploying a new version
- Datasets let you test expected in and output pairs and benchmark performance before deployiong ([Datasets](https://langfuse.com/docs/datasets))
- Track versions and releases in your application ([Experimentation](https://langfuse.com/docs/experimentation), [Prompt Management](https://langfuse.com/docs/prompts))
### Video: Langfuse in two minutes
https://github.com/langfuse/langfuse/assets/2834609/6041347a-b517-4a11-8737-93ef8f8af49f
_Muted by default, enable sound for voice-over_
## Get started
### Langfuse Cloud
Managed deployment by the Langfuse team, generous free-tier (hobby plan), no credit card required.
**[» Langfuse Cloud](https://cloud.langfuse.com)**
### Localhost (docker)
## Quick start
```bash
# Clone repository
git clone https://github.com/langfuse/langfuse.git
cd langfuse
# Run server and database
docker compose up -d
npm install
cp .env.example .env.local
# set NEXT_PUBLIC_IAM_CLIENT_ID for live sign-in; defaults point at production.
npm run dev # http://localhost:4000
```
[→ Learn more about deploying locally](https://langfuse.com/docs/deployment/local)
## Scripts
### Self-host (docker)
| Script | What |
| --- | --- |
| `npm run dev` | Dev server on :4000 |
| `npm run build` | Production build (type-checks; Gui CSS injected at runtime) |
| `npm run start` | Serve the production build |
| `npm run typecheck` | `tsc --noEmit` (strict) |
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container.
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
## Configuration
Templated deployments: [Railway, GCP Cloud Run, AWS Fargate, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
All config is `NEXT_PUBLIC_*` (browser app, cookie auth). See `.env.example`.
## Get Started
| Var | Default | Meaning |
| --- | --- | --- |
| `NEXT_PUBLIC_CLOUD_URL` | `https://cloud.hanzo.ai` | Unified `/v1` backend base URL |
| `NEXT_PUBLIC_IAM_URL` | `https://iam.hanzo.ai` | Hanzo IAM OIDC authority |
| `NEXT_PUBLIC_IAM_APP_NAME` | `hanzo-console` | IAM application (`<org>-<app>`) |
| `NEXT_PUBLIC_IAM_ORG_NAME` | `hanzo` | IAM organization |
| `NEXT_PUBLIC_IAM_CLIENT_ID` | — | OAuth client id |
### API Keys
## Architecture
You require a Langfuse public and secret key to get started. Sign up [here](https://cloud.langfuse.com) and find them in your project settings.
### Ingesting Data · Instrumenting Your Application
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
You may want to use another integration to get started quickly or implement a use case that we do not yet support. However, we recommend to migrate to the Langfuse SDKs over time to ensure performance and stability.
See our the [→ Quickstart](https://langfuse.com/docs/get-started) to get started in integrating Langfuse.
### Integrations
| Integration | Supports | Description |
| -------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- |
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](https://langfuse.com/docs/openai) | Python | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
External projects/packages that integrate with Langfuse:
| Name | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [LiteLLM](/https://langfuse.comdocs/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [Flowise](https://langfuse.com/docs/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](https://langfuse.com/docs/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
## Questions and feedback
### Ideas and roadmap
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
- [Feature Requests](https://langfuse.com/idea)
### Support and feedback
In order of preference the best way to communicate with us:
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions): Contribute [ideas](https://langfuse.com/idea) [support requests](https://github.com/orgs/langfuse/discussions/categories/support) and [report bugs](https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+) (preferred as we create a permanent, indexed artifact for other community members)
- [Discord](https://langfuse.com/discord): For community support and to chat directly with maintainers
- Privately: Email contact at langfuse dot com
## Contributing to Langfuse
- Vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
- Raise and comment on [Issues](https://github.com/langfuse/langfuse/issues)
- Open a PR - see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
See [LLM.md](./LLM.md) for the full design (base choice, /v1 client, auth flow,
the product-module registry, and the Providers surface). Endpoint reference in
[docs/endpoints.md](./docs/endpoints.md).
## License
This repository is MIT licensed, except for the `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
## Misc
### GET API to export your data
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics).
### Security & Privacy
We take data security and privacy seriously. Please refer to our [Security and Privacy](https://langfuse.com/security) page for more information.
### Telemetry
By default, Langfuse automatically reports basic usage statistics of self-hosted instances to a centralized server (PostHog).
This helps us to:
1. Understand how Langfuse is used and improve the most relevant features.
2. Track overall usage for internal and external (e.g. fundraising) reporting.
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
You can opt-out by setting `TELEMETRY_ENABLED=false`.
BSD-3-Clause. Copyright (c) 2026-present, Hanzo AI, Inc.
-4
View File
@@ -1,4 +0,0 @@
## Security Policy
We strongly recommend using the latest version of Langfuse to receive all security updates.
For more information, please refer to the [Data Security & Privacy](https://langfuse.com/docs/data-security-privacy) page in the documentation or contact security@langfuse.com.
+38
View File
@@ -0,0 +1,38 @@
'use client'
import { use } from 'react'
import { notFound } from 'next/navigation'
import { matchRoute } from '~/lib/products/match'
import { isAdminProductId } from '~/lib/auth/admin'
import { useSession } from '~/lib/auth/session'
import { ApiError } from '~/lib/api'
import { ErrorState } from '~/components/ui/States'
import { Loader } from '~/components/ui/Loader'
/**
* Catch-all product route. Resolves the module + route from the registry and
* renders its component. Adding a product anywhere in the registry makes its
* routes live here — no per-product page files.
*
* Function-level authz: an admin-only product (IAM/KMS/Secrets/Audit/Clusters/
* Kubernetes) NEVER renders for a non-admin, however the URL was reached — nav
* hiding is cosmetic, this is the gate. The backend `/v1` endpoints remain the
* server-side authority (defense in depth); this stops the admin UI from ever
* mounting (and firing those calls) for a non-admin.
*/
export default function ProductPage({ params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = use(params)
const { account, loading } = useSession()
const matched = matchRoute(slug)
if (!matched) notFound()
if (isAdminProductId(matched.module.id) && !account?.isAdmin) {
if (loading) return <Loader />
return <ErrorState err={new ApiError('Admin access is required for this surface.', 403)} />
}
const Component = matched.route.component
return <Component params={matched.params} />
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
import { use } from 'react'
import { ProductInterstitial } from '~/components/products/ProductInterstitial'
/**
* Product discover screen — `/discover/<id>` renders the interstitial for one
* catalog entry (docs, OSS source, revenue share, open/get-started). A dedicated
* route (more specific than the `[...slug]` product catch-all) so it's shareable.
*/
export default function DiscoverPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
return <ProductInterstitial id={id} />
}
+15
View File
@@ -0,0 +1,15 @@
import type { ReactNode } from 'react'
import { AuthGate } from '~/components/AuthGate'
import { DashboardShell } from '~/components/DashboardShell'
import { PreferencesProvider } from '~/lib/products/preferences'
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<AuthGate>
<PreferencesProvider>
<DashboardShell>{children}</DashboardShell>
</PreferencesProvider>
</AuthGate>
)
}
+143
View File
@@ -0,0 +1,143 @@
'use client'
/**
* Product catalog — the unified console home. Every Hanzo product, grouped by
* the ten canonical categories, with its enablement state and Google Cloud
* equivalent. `enabled` and `external` products open straight in (in-console or
* a new tab); `soon` products link to their discover screen. Each card can be
* pinned to the sidebar (persisted to the account). Rendered entirely from the
* catalog registry.
*/
import { useRouter } from 'next/navigation'
import { Button, Card, Text, XStack, YStack } from '@hanzo/gui'
import { Star, Lock, ExternalLink, ArrowRight, Info } from '@hanzogui/lucide-icons-2'
import { branding, config } from '~/config'
import { catalogByCategory, visibleCatalog, type CatalogEntry } from '~/lib/products/registry'
import { openProduct } from '~/lib/products/open'
import { useFavorites } from '~/lib/products/favorites'
import { useSession } from '~/lib/auth/session'
import { PageHeader } from '~/components/ui/PageHeader'
const STATUS_LABEL = { enabled: 'Enabled', external: 'External', soon: 'Soon' } as const
const STATUS_BG = { enabled: '$color5', external: '$color3', soon: '$color4' } as const
function StatusBadge({ entry }: { entry: CatalogEntry }) {
return (
<XStack bg={STATUS_BG[entry.status]} px="$2" py="$1" rounded="$10" items="center" gap="$1">
{entry.admin ? <Lock size={11} opacity={0.6} /> : null}
<Text fontSize="$1" color={entry.status === 'enabled' ? '$color12' : '$color11'} fontWeight="600">
{STATUS_LABEL[entry.status]}
</Text>
</XStack>
)
}
function ProductCard({
entry,
pinned,
onOpen,
onToggle,
onLearnMore,
}: {
entry: CatalogEntry
pinned: boolean
onOpen: () => void
onToggle: () => void
onLearnMore: () => void
}) {
const Icon = entry.icon
const openable = entry.status === 'enabled' || entry.status === 'external'
return (
<Card borderWidth={1} borderColor="$borderColor" p="$4" gap="$3" width={272}>
<XStack justify="space-between" items="flex-start">
<XStack gap="$2" items="center" flex={1}>
<Icon size={20} />
<YStack flex={1}>
<Text fontSize="$5" fontWeight="700">
{entry.label}
</Text>
{entry.gcp ? (
<Text fontSize="$1" color="$color10">
{entry.gcp}
</Text>
) : null}
</YStack>
</XStack>
<XStack gap="$1" items="center">
<Button
size="$2"
chromeless
opacity={0.4}
icon={<Info size={15} />}
onPress={onLearnMore}
aria-label={`Learn about ${entry.label}`}
/>
<Button
size="$2"
chromeless
opacity={pinned ? 1 : 0.3}
icon={<Star size={15} />}
onPress={onToggle}
aria-label={pinned ? `Unpin ${entry.label}` : `Pin ${entry.label}`}
/>
</XStack>
</XStack>
<Text fontSize="$3" color="$color11" minH={40}>
{entry.description}
</Text>
<XStack justify="space-between" items="center">
<StatusBadge entry={entry} />
<Button
size="$2"
bg={openable ? '$color5' : 'transparent'}
borderWidth={1}
borderColor="$borderColor"
onPress={openable ? onOpen : onLearnMore}
iconAfter={entry.kind === 'external' ? <ExternalLink size={14} /> : <ArrowRight size={14} />}
>
{openable ? 'Open' : 'Learn more'}
</Button>
</XStack>
</Card>
)
}
export default function DashboardHome() {
const router = useRouter()
const { account } = useSession()
const { toggle, isPinned } = useFavorites()
const push = (path: string) => router.push(path)
// Least privilege: non-admins don't see admin-only product cards on the home.
const groups = catalogByCategory(visibleCatalog(Boolean(account?.isAdmin)))
return (
<>
<PageHeader
title={branding.name}
subtitle={`See, enable, and manage every ${config.brandName} product from one place.`}
/>
{groups.map((group) => (
<YStack key={group.category} gap="$3">
<Text fontSize="$5" fontWeight="800" color="$color12">
{group.category}
</Text>
<XStack flexWrap="wrap" gap="$3">
{group.entries.map((entry) => (
<ProductCard
key={entry.id}
entry={entry}
pinned={isPinned(entry.id)}
onOpen={() => openProduct(entry, push)}
onToggle={() => toggle(entry.id)}
onLearnMore={() => push(`/discover/${entry.id}`)}
/>
))}
</XStack>
</YStack>
))}
</>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+79
View File
@@ -0,0 +1,79 @@
'use client'
/**
* IAM OAuth callback. IAM redirects here with `?code&state` (or `?error`). We:
* 1. surface any IdP `error`,
* 2. require `code` + `state`,
* 3. validate `state` against the value we stored at sign-in start (CSRF /
* authorization-code-injection defense) BEFORE exchanging the code,
* 4. exchange code (+ PKCE verifier) for a backend session, and land on `/`.
*
* The exchange runs exactly once (a ref guard) so React's dev double-effect can't
* consume the one-time state twice and false-flag a mismatch.
*/
import { Suspense, useEffect, useRef, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button, Text, YStack } from '@hanzo/gui'
import { ApiError } from '~/lib/api'
import { Loader } from '~/components/ui/Loader'
import { useSession } from '~/lib/auth/session'
import { consumeState, describeAuthError } from '~/lib/auth/iam'
function Callback() {
const params = useSearchParams()
const router = useRouter()
const { completeSignIn } = useSession()
const [error, setError] = useState<string | null>(null)
const ran = useRef(false)
useEffect(() => {
if (ran.current) return
ran.current = true
const idpError = params.get('error')
if (idpError) {
setError(describeAuthError(idpError, params.get('error_description')))
return
}
const code = params.get('code')
const state = params.get('state')
if (!code || !state) {
setError('Missing authorization code.')
return
}
// CSRF / code-injection defense: the returned state MUST match the one we
// stored when starting sign-in. Consume (clear) it either way.
const expected = consumeState()
if (!expected || state !== expected) {
setError('This sign-in could not be verified (state mismatch). Please sign in again.')
return
}
completeSignIn(code, state)
.then(() => router.replace('/'))
.catch((e: unknown) => setError(e instanceof ApiError ? e.message : 'Sign-in failed.'))
}, [params, completeSignIn, router])
if (error) {
return (
<YStack flex={1} minH="100vh" items="center" justify="center" gap="$3">
<Text color="$color12" fontWeight="600">
{error}
</Text>
<Button onPress={() => router.replace('/signin')}>Back to sign in</Button>
</YStack>
)
}
return <Loader label="Completing sign-in…" />
}
export default function CallbackPage() {
return (
<Suspense fallback={null}>
<Callback />
</Suspense>
)
}
+181
View File
@@ -0,0 +1,181 @@
/**
* Wallet HUSD top-up — verify on-chain, then record to commerce (server route).
*
* Why this lives in console2 and not billing: billing.hanzo.ai is a Next static
* export (`output: 'export'`) and cannot host a runtime POST handler, and the
* commerce backend is owned elsewhere. So the verify-and-record seam lives here
* as a same-origin server route — the same pattern as `app/paas/[...path]`: the
* browser calls the console's OWN origin, the server does the privileged work,
* and config comes from server-only env (sourced via KMS, never `NEXT_PUBLIC`).
*
* Flow: the client sends an HUSD ERC-20 transfer to the treasury and posts the
* tx hash here. We require a valid IAM session and derive the credited USER from
* it (NEVER the request body — that would be an IDOR). We read the receipt from
* the Hanzo EVM, confirm a mined, successful HUSD `Transfer(from → treasury,
* value)`, derive USD cents from the (18-decimal, USD-pegged) value, then record
* it to commerce as a `husd` crypto payment keyed by the tx hash as an
* idempotency key (replay-safe — the same tx never credits twice). The on-chain
* amount — never a client-supplied number — is what gets credited.
*
* Honest failure: if HUSD/treasury are unconfigured (greenfield) we return 501;
* if there is no session we return 401; if the tx is missing/failed/not an
* HUSD-to-treasury transfer we return 400; chain/commerce unreachable → 502.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { ethers } from 'ethers'
import { getServerAccount } from '~/lib/auth/server'
export const runtime = 'nodejs'
const ERC20_TRANSFER_ABI = ['event Transfer(address indexed from, address indexed to, uint256 value)']
const isAddr = (a: string): boolean => /^0x[0-9a-fA-F]{40}$/.test(a)
/** Forward the caller's identity (session cookie / bearer) to commerce. */
function authHeaders(req: NextRequest, extra: Record<string, string> = {}): Record<string, string> {
const h: Record<string, string> = { 'Content-Type': 'application/json', Accept: 'application/json', ...extra }
const cookie = req.headers.get('cookie')
if (cookie) h.Cookie = cookie
const auth = req.headers.get('authorization')
if (auth) h.Authorization = auth
return h
}
export async function POST(req: NextRequest): Promise<NextResponse> {
const HUSD_ADDRESS = (process.env.HANZO_HUSD_ADDRESS ?? '').trim()
const TREASURY = (process.env.HANZO_HUSD_TREASURY ?? '').trim()
const RPC_URL = (process.env.HANZO_RPC_URL ?? 'https://rpc.hanzo.network').replace(/\/+$/, '')
const COMMERCE_URL = (process.env.COMMERCE_URL ?? 'https://api.hanzo.ai').replace(/\/+$/, '')
const CHAIN_ID = Number(process.env.HANZO_CHAIN_ID ?? '36900')
// Greenfield gate: no HUSD contract / treasury ⇒ honest "not configured".
if (!isAddr(HUSD_ADDRESS) || !isAddr(TREASURY)) {
return NextResponse.json(
{ error: 'HUSD top-up is not configured yet (HUSD is not deployed on Hanzo Mainnet).' },
{ status: 501 },
)
}
// Authn: the credited user is the SESSION user — never the request body (IDOR).
const account = await getServerAccount(req.headers.get('cookie'))
if (!account) {
return NextResponse.json({ error: 'Sign in to top up your balance.' }, { status: 401 })
}
const userId = account.name
let body: { txHash?: string; fromAddress?: string }
try {
body = await req.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
}
const txHash = (body.txHash ?? '').trim()
const fromAddress = (body.fromAddress ?? '').trim()
if (!/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
return NextResponse.json({ error: 'A valid transaction hash is required.' }, { status: 400 })
}
// ── 1. Verify the HUSD transfer on-chain ────────────────────────────────────
let creditedCents: number
let verifiedFrom: string
try {
const provider = new ethers.JsonRpcProvider(RPC_URL, CHAIN_ID)
const receipt = await provider.getTransactionReceipt(txHash)
if (!receipt) {
return NextResponse.json({ error: 'Transaction not found or not yet mined.' }, { status: 400 })
}
if (receipt.status !== 1) {
return NextResponse.json({ error: 'Transaction failed on-chain.' }, { status: 400 })
}
const iface = new ethers.Interface(ERC20_TRANSFER_ABI)
const husd = HUSD_ADDRESS.toLowerCase()
const treasury = TREASURY.toLowerCase()
let value: bigint | null = null
for (const log of receipt.logs) {
if (log.address.toLowerCase() !== husd) continue
let parsed: ethers.LogDescription | null = null
try {
parsed = iface.parseLog({ topics: [...log.topics], data: log.data })
} catch {
continue
}
if (parsed?.name !== 'Transfer') continue
if (String(parsed.args.to).toLowerCase() !== treasury) continue
value = parsed.args.value as bigint
verifiedFrom = ethers.getAddress(String(parsed.args.from))
break
}
if (value === null) {
return NextResponse.json(
{ error: 'No HUSD transfer to the treasury was found in this transaction.' },
{ status: 400 },
)
}
if (fromAddress && isAddr(fromAddress) && verifiedFrom!.toLowerCase() !== fromAddress.toLowerCase()) {
return NextResponse.json({ error: 'Transfer sender does not match the connected wallet.' }, { status: 400 })
}
// HUSD is an 18-decimal, USD-pegged stablecoin → 1e16 base units = 1 cent.
creditedCents = Number(value / 10n ** 16n)
if (creditedCents <= 0) {
return NextResponse.json({ error: 'Transferred amount is below the minimum (1 cent).' }, { status: 400 })
}
} catch (e) {
return NextResponse.json(
{ error: `Could not verify the transaction on Hanzo Mainnet: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
// ── 2. Record to commerce as an HUSD crypto payment ─────────────────────────
// The tx hash is the idempotency key: a replay of the same hash MUST NOT credit
// twice. The hash is globally unique on-chain, so the ledger (commerce) dedupes
// on it — `Idempotency-Key` is the standard request for that guarantee.
try {
const recordRes = await fetch(`${COMMERCE_URL}/v1/billing/payment`, {
method: 'POST',
headers: authHeaders(req, { 'Idempotency-Key': txHash }),
cache: 'no-store',
body: JSON.stringify({
method: 'crypto',
network: 'hanzo',
chainId: CHAIN_ID,
currency: 'husd',
amount: creditedCents,
txHash,
fromAddress: verifiedFrom!,
toAddress: TREASURY,
userId,
}),
})
if (!recordRes.ok) {
const text = await recordRes.text().catch(() => '')
return NextResponse.json(
{ error: `Commerce rejected the payment (HTTP ${recordRes.status}): ${text}`.trim() },
{ status: 502 },
)
}
const payment = (await recordRes.json().catch(() => ({}))) as { status?: string }
// New balance (USD ledger) — best-effort; the credit already landed. The user
// is the SESSION user, never a client-supplied id.
let balance = 0
try {
const balRes = await fetch(
`${COMMERCE_URL}/v1/billing/balance?user=${encodeURIComponent(userId)}&currency=usd`,
{ headers: authHeaders(req), cache: 'no-store' },
)
if (balRes.ok) balance = ((await balRes.json()) as { balance?: number }).balance ?? 0
} catch {
/* balance is informational; the credit is recorded */
}
return NextResponse.json({ creditedCents, balance, txHash, status: payment.status ?? 'recorded' })
} catch (e) {
return NextResponse.json(
{ error: `Could not reach commerce to record the payment: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+17
View File
@@ -0,0 +1,17 @@
html,
body,
#__next {
height: 100%;
}
body {
margin: 0;
background-color: var(--background, #070b13);
color: var(--color, #f2f2f2);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
}
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 67" role="img" aria-label="Hanzo">
<style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style>
<path d="M22.21 67V44.6369H0V67H22.21Z"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path d="M22.21 0H0V22.3184H22.21V0Z"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

+27
View File
@@ -0,0 +1,27 @@
import '@hanzogui/core/reset.css'
import './globals.css'
import type { Metadata, Viewport } from 'next'
import type { ReactNode } from 'react'
import { Provider } from '~/components/Provider'
import { branding } from '~/config'
export const metadata: Metadata = {
title: branding.name,
description: 'Unified admin console for Hanzo Cloud and all cloud products.',
}
export const viewport: Viewport = {
themeColor: '#000000',
}
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className="t_dark" style={{ backgroundColor: '#070b13', colorScheme: 'dark' }} suppressHydrationWarning>
<body style={{ margin: 0 }}>
<Provider>{children}</Provider>
</body>
</html>
)
}
+102
View File
@@ -0,0 +1,102 @@
/**
* Same-origin proxy to the platform.hanzo.ai control plane (embedded PaaS). The
* browser calls console2's OWN origin (`/paas/...`); this server-side handler
* forwards to `platform.hanzo.ai/v1/...`, injecting the service token from
* server-only env (sourced via KMS — never `NEXT_PUBLIC_`, never in the browser
* bundle). This is the real control-plane API, not an iframe stub.
*
* SECURITY (deny-by-default): the proxy attaches a powerful, UNSCOPED platform
* service token (every tenant, every cluster), so EVERY request must first
* present a valid IAM session AND be a GLOBAL platform admin — both verified
* BEFORE the token is attached. The token carries no user scope, so the gate is
* the sole authz: a tenant ORG admin (`isAdmin`) is NOT enough; only a global
* admin (member of the admin org) passes. Unauthenticated → 401, non-global-admin
* → 403. The forwarded path is constrained to `/v1/...` (no `..` traversal).
* (Re-add `PAAS_SERVICE_TOKEN` to the deployment once this gate is confirmed.)
*
* When `PAAS_SERVICE_TOKEN` is unset the proxy returns an honest 501 (to global
* admins) so the UI shows a truthful "not configured" state — it never fabricates.
*/
import { type NextRequest, NextResponse } from 'next/server'
import { getServerAccount, isGlobalAdminAccount } from '~/lib/auth/server'
/**
* A path segment is safe iff it cannot alter the resolved path: non-empty, not a
* relative `.`/`..`, and free of separators / NUL. This keeps the forwarded URL
* inside the platform's `/v1/` prefix (URL normalization would let `..` escape it).
*/
const isSafeSegment = (s: string): boolean =>
s.length > 0 && s !== '.' && s !== '..' && !s.includes('/') && !s.includes('\\') && !s.includes('\0')
async function forward(req: NextRequest, path: string[]): Promise<NextResponse> {
// Deny-by-default authz — verify the session + require GLOBAL admin BEFORE the
// token (uniform 401 for anyone unauthenticated, regardless of the path shape).
const account = await getServerAccount(req.headers.get('cookie'))
if (!account) {
return NextResponse.json({ error: 'Sign in to use the control plane.' }, { status: 401 })
}
if (!isGlobalAdminAccount(account)) {
return NextResponse.json(
{ error: 'Global platform admin access is required for the control plane.' },
{ status: 403 },
)
}
// Constrain the forwarded path to /v1/... — reject any traversal/odd segment.
if (!path.every(isSafeSegment)) {
return NextResponse.json({ error: 'Invalid control-plane path.' }, { status: 400 })
}
const token = process.env.PAAS_SERVICE_TOKEN ?? ''
if (!token) {
return NextResponse.json(
{ error: 'PaaS control plane is not configured (PAAS_SERVICE_TOKEN missing).' },
{ status: 501 },
)
}
const platformUrl = (process.env.PLATFORM_URL ?? 'https://platform.hanzo.ai').replace(/\/+$/, '')
const url = `${platformUrl}/v1/${path.join('/')}${req.nextUrl.search}`
const init: RequestInit = {
method: req.method,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
// Never cache control-plane reads.
cache: 'no-store',
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
init.body = await req.text()
}
try {
const res = await fetch(url, init)
const text = await res.text()
return new NextResponse(text, {
status: res.status,
headers: { 'Content-Type': res.headers.get('content-type') ?? 'application/json' },
})
} catch (e) {
return NextResponse.json(
{ error: `PaaS upstream unreachable: ${e instanceof Error ? e.message : String(e)}` },
{ status: 502 },
)
}
}
type Ctx = { params: Promise<{ path: string[] }> }
export async function GET(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function POST(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function PATCH(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
export async function DELETE(req: NextRequest, ctx: Ctx) {
return forward(req, (await ctx.params).path)
}
+18
View File
@@ -0,0 +1,18 @@
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { SignInForm } from '~/components/SignInForm'
import { useSession } from '~/lib/auth/session'
export default function SignInPage() {
const { account, loading } = useSession()
const router = useRouter()
useEffect(() => {
if (!loading && account) router.replace('/')
}, [loading, account, router])
return <SignInForm />
}
-15
View File
@@ -1,15 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/src/components",
"utils": "@/src/utils/tailwind"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

-35
View File
@@ -1,35 +0,0 @@
version: "3.5"
services:
langfuse-server:
build:
dockerfile: Dockerfile
depends_on:
- db
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
restart: always
db:
image: postgres
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
volumes:
database_data:
driver: local
-19
View File
@@ -1,19 +0,0 @@
version: "3.5"
services:
db:
image: postgres
restart: always
command: ["postgres", "-c", "log_statement=all"]
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
volumes:
database_data:
driver: local
-33
View File
@@ -1,33 +0,0 @@
version: "3.5"
services:
langfuse-server:
image: ghcr.io/langfuse/langfuse:latest
depends_on:
- db
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
db:
image: postgres
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
volumes:
database_data:
driver: local
+74
View File
@@ -0,0 +1,74 @@
# Unified `/v1` backend endpoints
The console talks to the unified Hanzo Cloud backend (`hanzoai/cloud`, the
casibase API). Base URL: `${NEXT_PUBLIC_CLOUD_URL}/v1`. All requests send cookie
credentials; responses are the envelope `{ status, msg, data, data2 }` (`data2`
is the total row count on list endpoints).
Client modules live in `src/lib/api/`.
## Account / session — `AccountApi`
| Method | Endpoint |
| --- | --- |
| `current()` | `GET /get-account` |
| `signin(code, state)` | `POST /signin?code&state` |
| `signout()` | `POST /signout` |
## Providers — `ProviderApi`
| Method | Endpoint |
| --- | --- |
| `listGlobal()` | `GET /get-global-providers` |
| `list({ owner, store, p, pageSize, … })` | `GET /get-providers` |
| `get(owner, name)` | `GET /get-provider?id=owner/name` |
| `add(p)` | `POST /add-provider` |
| `update(owner, name, p)` | `POST /update-provider?id=owner/name` |
| `remove(p)` | `POST /delete-provider` |
| `refreshMcpTools(p)` | `POST /refresh-mcp-tools` |
## Model routes — `ModelRouteApi`
| Method | Endpoint |
| --- | --- |
| `list({ owner, … })` | `GET /get-model-routes` |
| `get(owner, modelName)` | `GET /get-model-route?owner&modelName` |
| `add(r)` | `POST /add-model-route` |
| `update(owner, modelName, r)` | `POST /update-model-route?owner&modelName` |
| `remove(r)` | `POST /delete-model-route` |
## Applications — `ApplicationApi`
| Method | Endpoint |
| --- | --- |
| `list({ owner, … })` | `GET /get-applications` |
| `get(owner, name)` | `GET /get-application?id=owner/name` |
| `add(a)` | `POST /add-application` |
| `update(owner, name, a)` | `POST /update-application?id=owner/name` |
| `remove(a)` | `POST /delete-application` |
| `deploy(a)` | `POST /deploy-application?id=owner/name` |
| `undeploy(owner, name)` | `POST /undeploy-application?id=owner/name` |
## Stores — `StoreApi`
| Method | Endpoint |
| --- | --- |
| `listGlobal()` | `GET /get-global-stores` |
| `list(owner)` | `GET /get-stores?owner` |
| `get(owner, name)` | `GET /get-store?id=owner/name` |
| `names(owner)` | `GET /get-store-names?owner` |
| `add(s)` | `POST /add-store` |
| `update(owner, name, s)` | `POST /update-store?id=owner/name` |
| `remove(s)` | `POST /delete-store` |
| `refreshVectors(s)` | `POST /refresh-store-vectors` |
## Chat — `ChatApi`
| Method | Endpoint |
| --- | --- |
| `listGlobal({ … })` | `GET /get-global-chats` |
| `list({ user, store, selectedUser, … })` | `GET /get-chats` |
| `get(owner, name)` | `GET /get-chat?id=owner/name` |
| `add(c)` | `POST /add-chat` |
| `update(owner, name, c)` | `POST /update-chat?id=owner/name` |
| `remove(c)` | `POST /delete-chat` |
-3
View File
@@ -1,3 +0,0 @@
# Enterprise Edition
While we are currently working 100% on the open source version, we consider financing the project by developing some premium features as part of an enterprise version under a commercial license. Please reach out to us if you have specific requirements: enterprise@langfuse.com
-33
View File
@@ -1,33 +0,0 @@
#!/bin/sh
# Check if DATABASE_URL is not set
if [ -z "$DATABASE_URL" ]; then
# Check if all required variables are provided
if [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then
# Construct DATABASE_URL from the provided variables
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"
export DATABASE_URL
else
echo "Error: Required database environment variables are not set. Provide a postgres url for DATABASE_URL."
exit 1
fi
fi
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
if [ -z "$DIRECT_URL" ]; then
export DIRECT_URL=$DATABASE_URL
fi
# Apply migrations
prisma migrate deploy
status=$?
# If migration fails (returns non-zero exit status), exit script with that status
if [ $status -ne 0 ]; then
echo "Applying database migrations failed. This is mostly caused by the database being unavailable."
echo "Exiting..."
exit $status
fi
# Start server
node server.js
-11
View File
@@ -1,11 +0,0 @@
name: langfuse
error-discrimination:
strategy: status-code
auth: bearer
imports:
commons: commons.yml
errors:
- commons.Error
- commons.UnauthorizedError
- commons.AccessDeniedError
- commons.MethodNotAllowedError
-13
View File
@@ -1,13 +0,0 @@
errors:
Error:
status-code: 400
type: string
UnauthorizedError:
status-code: 401
type: string
AccessDeniedError:
status-code: 403
type: string
MethodNotAllowedError:
status-code: 405
type: string
-31
View File
@@ -1,31 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Add a score to the database, upserts on id
method: POST
path: /scores
request: CreateScoreRequest
response: Score
types:
CreateScoreRequest:
properties:
id: optional<string>
traceId: string
name: string
value: double
observationId: optional<string>
comment: optional<string>
Score:
properties:
id: string
traceId: string
name: string
value: double
observationId: optional<string>
timestamp: datetime
comment: optional<string>
-21
View File
@@ -1,21 +0,0 @@
default-group: local
groups:
local:
generators:
# - name: fernapi/fern-typescript-browser-sdk
# version: 0.7.1
# output:
# location: npm
# url: npm.buildwithfern.com
# package-name: "@finto-fern/react-client"
# config:
# namespaceExport: Langfuse
# allowCustomFetcher: true
- name: fernapi/fern-openapi
version: 0.0.26
output:
location: local-file-system
path: ../../../generated/openapi-client
config:
namespaceExport: Langfuse
allowCustomFetcher: true
-23
View File
@@ -1,23 +0,0 @@
name: langfuse
docs: |
## Authentication
Authenticate with the API using Basic Auth, get API keys in the project settings:
- username: Langfuse Public Key
- password: Langfuse Secret Key
error-discrimination:
strategy: status-code
auth: basic
imports:
commons: commons.yml
errors:
- commons.Error
- commons.UnauthorizedError
- commons.AccessDeniedError
- commons.MethodNotAllowedError
- commons.NotFoundError
headers:
X-Langfuse-Sdk-Name: optional<string>
X-Langfuse-Sdk-Version: optional<string>
X-Langfuse-Public-Key: optional<string>
-171
View File
@@ -1,171 +0,0 @@
types:
# Objects
Trace:
properties:
id:
type: string
docs: The unique identifier of a trace
timestamp: datetime
name: optional<string>
input: optional<unknown>
output: optional<unknown>
sessionId: optional<string>
release: optional<string>
version: optional<string>
userId: optional<string>
metadata: optional<unknown>
tags: optional<list<string>>
public:
type: optional<boolean>
docs: Public traces are accessible via url without login
TraceWithDetails:
extends: Trace
properties:
observations:
type: list<string>
docs: List of observation ids
scores:
type: list<string>
docs: List of score ids
TraceWithFullDetails:
extends: Trace
properties:
observations: list<ObservationsView>
scores: list<Score>
Session:
properties:
id: string
createdAt: datetime
projectId: string
SessionWithTraces:
extends: Session
properties:
traces: list<Trace>
Observation:
properties:
id: string
traceId: optional<string>
type: string
name: optional<string>
startTime: datetime
endTime: optional<datetime>
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, MapValue>>
input: optional<unknown>
version: optional<string>
metadata: optional<unknown>
output: optional<unknown>
usage: optional<Usage>
level: ObservationLevel
statusMessage: optional<string>
parentObservationId: optional<string>
promptId: optional<string>
ObservationsView:
extends: Observation
properties:
modelId: optional<string>
inputPrice: optional<double>
outputPrice: optional<double>
totalPrice: optional<double>
calculatedInputCost: optional<double>
calculatedOutputCost: optional<double>
calculatedTotalCost: optional<double>
Usage:
properties:
input: optional<integer>
output: optional<integer>
total: optional<integer>
unit: optional<ModelUsageUnit>
inputCost: optional<double>
outputCost: optional<double>
totalCost: optional<double>
Score:
properties:
id: string
traceId: string
name: string
value: double
observationId: optional<string>
timestamp: datetime
comment: optional<string>
Dataset:
properties:
id: string
name: string
projectId: string
createdAt: datetime
updatedAt: datetime
items: list<DatasetItem>
runs: list<string>
DatasetItem:
properties:
id: string
status: DatasetStatus
input: unknown
expectedOutput: optional<unknown>
sourceObservationId: optional<string>
datasetId: string
createdAt: datetime
updatedAt: datetime
DatasetRunItem:
properties:
id: string
datasetRunId: string
datasetItemId: string
observationId: string
createdAt: datetime
updatedAt: datetime
DatasetRun:
properties:
id: string
name: string
datasetId: string
createdAt: datetime
updatedAt: datetime
datasetRunItems: list<DatasetRunItem>
# Utilities
ModelUsageUnit:
enum:
- CHARACTERS
- TOKENS
- MILLISECONDS
- SECONDS
- IMAGES
ObservationLevel:
enum:
- DEBUG
- DEFAULT
- WARNING
- ERROR
MapValue:
discriminated: false
union:
- optional<string>
- optional<integer>
- optional<boolean>
- optional<list<string>>
DatasetStatus:
enum:
- ACTIVE
- ARCHIVED
errors:
Error:
status-code: 400
type: unknown
UnauthorizedError:
status-code: 401
type: unknown
AccessDeniedError:
status-code: 403
type: unknown
NotFoundError:
status-code: 404
type: unknown
MethodNotAllowedError:
status-code: 405
type: unknown
@@ -1,29 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
method: POST
docs: Create a dataset item, upserts on id
path: /dataset-items
request: CreateDatasetItemRequest
response: commons.DatasetItem
get:
docs: Get a specific dataset item
method: GET
path: /dataset-items/{id}
path-parameters:
id:
type: string
response: commons.DatasetItem
types:
CreateDatasetItemRequest:
properties:
datasetName: string
input: unknown
expectedOutput: optional<unknown>
id: optional<string>
@@ -1,19 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
method: POST
docs: Create a dataset run item
path: /dataset-run-items
request: CreateDatasetRunItemRequest
response: commons.DatasetRunItem
types:
CreateDatasetRunItemRequest:
properties:
runName: string
datasetItemId: string
observationId: string
-33
View File
@@ -1,33 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
method: GET
docs: Get a dataset and its items
path: /datasets/{datasetName}
path-parameters:
datasetName: string
response: commons.Dataset
create:
method: POST
docs: Create a dataset
path: /datasets
request: CreateDatasetRequest
response: commons.Dataset
getRuns:
method: GET
docs: Get a dataset run and its items
path: /datasets/{datasetName}/runs/{runName}
path-parameters:
datasetName: string
runName: string
response: commons.DatasetRun
types:
CreateDatasetRequest:
properties:
name: string
-28
View File
@@ -1,28 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
service:
auth: false
base-path: /api/public
endpoints:
health:
docs: Check health of API and database
method: GET
path: /health
response: HealthResponse
errors:
- ServiceUnavailableError
types:
HealthResponse:
properties:
version:
type: string
docs: Langfuse server version
status: string
examples:
- value:
version: 1.25.0
status: OK
errors:
ServiceUnavailableError:
status-code: 503
-228
View File
@@ -1,228 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
batch:
docs: Ingest multiple events to Langfuse
method: POST
path: /ingestion
request:
name: IngestionRequest
body:
properties:
batch: list<IngestionEvent>
response: IngestionResponse # will be reportet as 200 response, but endpoint returns 207
types:
IngestionEvent:
discriminant: "type"
union:
trace-create: TraceEvent
score-create: ScoreEvent
event-create: CreateEventEvent
generation-create: CreateGenerationEvent
generation-update: UpdateGenerationEvent
span-create: CreateSpanEvent
span-update: UpdateSpanEvent
sdk-log: SDKLogEvent
# both are legacy
observation-create: CreateObservationEvent
observation-update: UpdateObservationEvent
ObservationType:
enum:
- SPAN
- GENERATION
- EVENT
IngestionUsage:
discriminated: false
union:
- commons.Usage
- OpenAIUsage
OpenAIUsage:
properties:
promptTokens: optional<integer>
completionTokens: optional<integer>
totalTokens: optional<integer>
OptionalObservationBody:
properties:
traceId: optional<string>
name: optional<string>
startTime: optional<datetime>
metadata: optional<unknown>
input: optional<unknown>
output: optional<unknown>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
version: optional<string>
CreateEventBody:
extends: OptionalObservationBody
properties:
id: optional<string>
UpdateEventBody:
extends: OptionalObservationBody
properties:
id: string
CreateSpanBody:
extends: CreateEventBody
properties:
endTime: optional<datetime>
UpdateSpanBody:
extends: UpdateEventBody
properties:
endTime: optional<datetime>
CreateGenerationBody:
extends: CreateSpanBody
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
UpdateGenerationBody:
extends: UpdateSpanBody
properties:
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
promptVersion: optional<integer>
ObservationBody:
properties:
id: optional<string>
traceId: optional<string>
type: ObservationType
name: optional<string>
startTime: optional<datetime>
endTime: optional<datetime>
completionStartTime: optional<datetime>
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
input: optional<unknown>
version: optional<string>
metadata: optional<unknown>
output: optional<unknown>
usage: optional<commons.Usage>
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
TraceBody:
properties:
id: optional<string>
name: optional<string>
userId: optional<string>
input: optional<unknown>
output: optional<unknown>
sessionId: optional<string>
release: optional<string>
version: optional<string>
metadata: optional<unknown>
tags: optional<list<string>>
public:
type: optional<boolean>
docs: Make trace publicly accessible via url
SDKLogBody:
properties:
log: unknown
ScoreBody:
properties:
id: optional<string>
traceId: string
name: string
value: double
observationId: optional<string>
comment: optional<string>
BaseEvent:
properties:
id: string
timestamp: string
metadata: unknown
TraceEvent:
extends: BaseEvent
properties:
body: TraceBody
CreateObservationEvent:
extends: BaseEvent
properties:
body: ObservationBody
UpdateObservationEvent:
extends: BaseEvent
properties:
body: ObservationBody
ScoreEvent:
extends: BaseEvent
properties:
body: ScoreBody
SDKLogEvent:
extends: BaseEvent
properties:
body: SDKLogBody
CreateGenerationEvent:
extends: BaseEvent
properties:
body: CreateGenerationBody
UpdateGenerationEvent:
extends: BaseEvent
properties:
body: UpdateGenerationBody
CreateSpanEvent:
extends: BaseEvent
properties:
body: CreateSpanBody
UpdateSpanEvent:
extends: BaseEvent
properties:
body: UpdateSpanBody
CreateEventEvent:
extends: BaseEvent
properties:
body: CreateEventBody
IngestionSuccess:
properties:
id: string
status: integer
IngestionError:
properties:
id: string
status: integer
message: optional<string>
error: optional<unknown>
IngestionResponse:
properties:
successes: list<IngestionSuccess>
errors: list<IngestionError>
@@ -1,43 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a specific observation
method: GET
path: /observations/{observationId}
path-parameters:
observationId:
type: string
docs: The unique langfuse identifier of an observation, can be an event, span or generation
response: commons.ObservationsView
getMany:
docs: Get a list of observations
method: GET
path: /observations
request:
name: GetObservationsRequest
query-parameters:
page: optional<integer>
limit: optional<integer>
name: optional<string>
userId: optional<string>
type: optional<string>
traceId: optional<string>
parentObservationId: optional<string>
response: ObservationsViews
types:
Observations:
properties:
data: list<commons.Observation>
meta: pagination.MetaResponse
ObservationsViews:
properties:
data: list<commons.ObservationsView>
meta: pagination.MetaResponse
-22
View File
@@ -1,22 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
method: GET
path: /projects
response: Projects
types:
Projects:
properties:
data: list<Project>
Project:
properties:
id: string
name: string
-36
View File
@@ -1,36 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a specific prompt
method: GET
path: /prompts
request:
name: GetParameterRequest
query-parameters:
name: string
version: optional<integer>
response: Prompt
create:
docs: Create a specific prompt
method: POST
path: /prompts
request: CreatePromptRequest
response: Prompt
types:
CreatePromptRequest:
properties:
name: string
isActive: boolean
prompt: string
Prompt:
properties:
name: string
version: integer
prompt: string
-39
View File
@@ -1,39 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
create:
docs: Add a score to the database, upserts on id
method: POST
path: /scores
request: CreateScoreRequest
response: commons.Score
get:
docs: Get scores
method: GET
path: /scores
request:
name: GetScoresRequest
query-parameters:
page: optional<integer>
limit: optional<integer>
userId: optional<string>
name: optional<string>
response: Scores
types:
CreateScoreRequest:
properties:
id: optional<string>
traceId: string
name: string
value: double
observationId: optional<string>
comment: optional<string>
Scores:
properties:
data: list<commons.Score>
meta: pagination.MetaResponse
-17
View File
@@ -1,17 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a session
method: GET
path: /sessions/{sessionId}
path-parameters:
sessionId:
type: string
docs: The unique id of a session
response: commons.SessionWithTraces
-45
View File
@@ -1,45 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
pagination: ./utils/pagination.yml
commons: ./commons.yml
service:
auth: true
base-path: /api/public
endpoints:
get:
docs: Get a specific trace
method: GET
path: /traces/{traceId}
path-parameters:
traceId:
type: string
docs: The unique langfuse identifier of a trace
response: commons.TraceWithFullDetails
list:
docs: Get list of traces
method: GET
path: /traces
request:
name: GetTracesRequest
query-parameters:
page: optional<integer>
limit: optional<integer>
userId: optional<string>
name: optional<string>
orderBy:
type: string
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
tags:
type: optional<string>
allow-multiple: true
docs: Only traces that include all of these tags will be returned.
response: Traces
types:
Traces:
properties:
data: list<commons.TraceWithDetails>
meta: pagination.MetaResponse
Sort:
properties:
id: string
@@ -1,15 +0,0 @@
types:
MetaResponse:
properties:
page:
type: integer
docs: current page number
limit:
type: integer
docs: number of items per page
totalItems:
type: integer
docs: number of total items given the current filters/selection (if any)
totalPages:
type: integer
docs: number of total pages given the current limit
-54
View File
@@ -1,54 +0,0 @@
default-group: local
groups:
local:
generators:
- name: fernapi/fern-openapi
version: 0.0.28
output:
location: local-file-system
path: ../../../generated/openapi-server
config:
namespaceExport: Langfuse
allowCustomFetcher: true
- name: fernapi/fern-python-sdk
version: 0.7.3
output:
location: local-file-system
path: ../../../generated/python
config:
client_class_name: FernLangfuse
pydantic_config:
require_optional_fields: false
- name: fernapi/fern-typescript-node-sdk
version: 0.7.1
output:
location: local-file-system
path: ../../../generated/typescript-server
config:
namespaceExport: Langfuse
allowCustomFetcher: true
- name: fernapi/fern-postman
version: 0.0.45
output:
location: local-file-system
path: ../../../generated/postman
# published:
# generators:
# - name: fernapi/fern-python-sdk
# version: 0.3.7
# output:
# location: pypi
# url: pypi.buildwithfern.com
# package-name: finto-fern-langfuse
# config:
# namespaceExport: Langfuse
# allowCustomFetcher: true
# - name: fernapi/fern-typescript-node-sdk
# version: 0.7.1
# output:
# location: npm
# url: npm.buildwithfern.com
# package-name: "@finto-fern/langfuse-node"
# config:
# namespaceExport: Langfuse
# allowCustomFetcher: true
-4
View File
@@ -1,4 +0,0 @@
{
"organization": "finto",
"version": "0.16.36"
}
-1
View File
@@ -1 +0,0 @@
python/
-104
View File
@@ -1,104 +0,0 @@
openapi: 3.0.1
info:
title: langfuse
version: ''
paths:
/api/public/scores:
post:
description: Add a score to the database, upserts on id
operationId: score_create
tags:
- Score
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/Score'
'400':
description: ''
content:
application/json:
schema:
type: string
'401':
description: ''
content:
application/json:
schema:
type: string
'403':
description: ''
content:
application/json:
schema:
type: string
'405':
description: ''
content:
application/json:
schema:
type: string
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateScoreRequest'
components:
schemas:
CreateScoreRequest:
title: CreateScoreRequest
type: object
properties:
id:
type: string
traceId:
type: string
name:
type: string
value:
type: number
format: double
observationId:
type: string
comment:
type: string
required:
- traceId
- name
- value
Score:
title: Score
type: object
properties:
id:
type: string
traceId:
type: string
name:
type: string
value:
type: number
format: double
observationId:
type: string
timestamp:
type: string
format: date-time
comment:
type: string
required:
- id
- traceId
- name
- value
- timestamp
securitySchemes:
BearerAuth:
type: http
scheme: bearer
File diff suppressed because it is too large Load Diff
-764
View File
@@ -1,764 +0,0 @@
{
"info": {
"name": "Langfuse",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": "## Authentication\n\nAuthenticate with the API using Basic Auth, get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key"
},
"variable": [
{
"key": "baseUrl",
"value": "",
"type": "string"
},
{
"key": "username",
"value": "",
"type": "string"
},
{
"key": "password",
"value": "",
"type": "string"
}
],
"auth": {
"type": "basic",
"basic": [
{
"key": "username",
"value": "{{username}}",
"type": "string"
},
{
"key": "password",
"value": "{{password}}",
"type": "string"
}
]
},
"item": [
{
"_type": "container",
"description": null,
"name": "Dataset Items",
"item": [
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a dataset item, upserts on id",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-items",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"dataset-items"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific dataset item",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-items/:id",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"dataset-items",
":id"
],
"query": [],
"variable": [
{
"key": "id",
"value": "",
"description": null
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Dataset Run Items",
"item": [
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a dataset run item",
"url": {
"raw": "{{baseUrl}}/api/public/dataset-run-items",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"dataset-run-items"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"runName\": \"example\",\n \"datasetItemId\": \"example\",\n \"observationId\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Datasets",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a dataset and its items",
"url": {
"raw": "{{baseUrl}}/api/public/datasets/:datasetName",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"datasets",
":datasetName"
],
"query": [],
"variable": [
{
"key": "datasetName",
"value": "",
"description": null
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
},
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a dataset",
"url": {
"raw": "{{baseUrl}}/api/public/datasets",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"datasets"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Get Runs",
"request": {
"description": "Get a dataset run and its items",
"url": {
"raw": "{{baseUrl}}/api/public/datasets/:datasetName/runs/:runName",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"datasets",
":datasetName",
"runs",
":runName"
],
"query": [],
"variable": [
{
"key": "datasetName",
"value": "",
"description": null
},
{
"key": "runName",
"value": "",
"description": null
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Health",
"item": [
{
"_type": "endpoint",
"name": "Health",
"request": {
"description": "Check health of API and database",
"url": {
"raw": "{{baseUrl}}/api/public/health",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"health"
],
"query": [],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Ingestion",
"item": [
{
"_type": "endpoint",
"name": "Batch",
"request": {
"description": "Ingest multiple events to Langfuse",
"url": {
"raw": "{{baseUrl}}/api/public/ingestion",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"ingestion"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Observations",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific observation",
"url": {
"raw": "{{baseUrl}}/api/public/observations/:observationId",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"observations",
":observationId"
],
"query": [],
"variable": [
{
"key": "observationId",
"value": "",
"description": "The unique langfuse identifier of an observation, can be an event, span or generation"
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
},
{
"_type": "endpoint",
"name": "Get Many",
"request": {
"description": "Get a list of observations",
"url": {
"raw": "{{baseUrl}}/api/public/observations?page=&limit=&name=&userId=&type=&traceId=&parentObservationId=",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"observations"
],
"query": [
{
"key": "page",
"value": "",
"description": null
},
{
"key": "limit",
"value": "",
"description": null
},
{
"key": "name",
"value": "",
"description": null
},
{
"key": "userId",
"value": "",
"description": null
},
{
"key": "type",
"value": "",
"description": null
},
{
"key": "traceId",
"value": "",
"description": null
},
{
"key": "parentObservationId",
"value": "",
"description": null
}
],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Projects",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": null,
"url": {
"raw": "{{baseUrl}}/api/public/projects",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"projects"
],
"query": [],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Prompts",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific prompt",
"url": {
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"prompts"
],
"query": [
{
"key": "name",
"value": "",
"description": null
},
{
"key": "version",
"value": "",
"description": null
}
],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
},
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Create a specific prompt",
"url": {
"raw": "{{baseUrl}}/api/public/prompts",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"prompts"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Score",
"item": [
{
"_type": "endpoint",
"name": "Create",
"request": {
"description": "Add a score to the database, upserts on id",
"url": {
"raw": "{{baseUrl}}/api/public/scores",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"scores"
],
"query": [],
"variable": []
},
"header": [],
"method": "POST",
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"value\": 0,\n \"observationId\": \"example\",\n \"comment\": \"example\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
},
"response": []
},
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get scores",
"url": {
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"scores"
],
"query": [
{
"key": "page",
"value": "",
"description": null
},
{
"key": "limit",
"value": "",
"description": null
},
{
"key": "userId",
"value": "",
"description": null
},
{
"key": "name",
"value": "",
"description": null
}
],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Sessions",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a session",
"url": {
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"sessions",
":sessionId"
],
"query": [],
"variable": [
{
"key": "sessionId",
"value": "",
"description": "The unique id of a session"
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
},
{
"_type": "container",
"description": null,
"name": "Trace",
"item": [
{
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a specific trace",
"url": {
"raw": "{{baseUrl}}/api/public/traces/:traceId",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"traces",
":traceId"
],
"query": [],
"variable": [
{
"key": "traceId",
"value": "",
"description": "The unique langfuse identifier of a trace"
}
]
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
},
{
"_type": "endpoint",
"name": "List",
"request": {
"description": "Get list of traces",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"public",
"traces"
],
"query": [
{
"key": "page",
"value": "",
"description": null
},
{
"key": "limit",
"value": "",
"description": null
},
{
"key": "userId",
"value": "",
"description": null
},
{
"key": "name",
"value": "",
"description": null
},
{
"key": "orderBy",
"value": "",
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
},
{
"key": "tags",
"value": "",
"description": "Only traces that include all of these tags will be returned."
}
],
"variable": []
},
"header": [],
"method": "GET",
"auth": null,
"body": null
},
"response": []
}
]
}
]
}
+8
View File
@@ -0,0 +1,8 @@
import { defaultConfig } from '@hanzogui/config/v5'
import { createGui } from '@hanzo/gui'
export const config = createGui(defaultConfig)
export default config
export type Conf = typeof config
Vendored
+11
View File
@@ -0,0 +1,11 @@
/**
* Registers our Gui config with the type system so component style props
* (tokens, themes, shorthands) are typed. `GuiCustomConfig` is declared in
* `@hanzogui/web` and re-exported across the Gui packages; augmenting it there
* flows the types through every `@hanzo/gui` component.
*/
import type { Conf } from './gui.config'
declare module '@hanzogui/web' {
interface GuiCustomConfig extends Conf {}
}
-38
View File
@@ -1,38 +0,0 @@
// jest.config.mjs
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
const clientTestConfig = {
displayName: "client",
testMatch: ["/**/*.clienttest.[jt]s?(x)"],
testEnvironment: "jest-environment-jsdom",
};
const serverTestConfig = {
displayName: "server",
testMatch: ["/**/*.servertest.[jt]s?(x)"],
testEnvironment: "jest-environment-node",
};
// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
const esModules = ["superjson"];
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const config = {
// Add more setup options before each test is run
silent: false,
verbose: true,
projects: [
await createJestConfig(clientTestConfig)(),
{
...(await createJestConfig(serverTestConfig)()),
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
},
],
};
export default config;
+98 -94
View File
@@ -1,103 +1,107 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.mjs");
import { withSentryConfig } from "@sentry/nextjs";
import { env } from "./src/env.mjs";
import { readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
/** @type {import("next").NextConfig} */
/**
* Hanzo Cloud Console — Next.js config.
*
* Hanzo GUI is consumed at runtime (no optimizing compiler): the published
* `@hanzogui/next-plugin` has a broken npm dependency (`hanzogui-loader@7.3.0`
* is unpublished; the available fork renames its exports), so we transpile the
* Gui ESM packages with Next's built-in `transpilePackages` and let
* `GuiProvider` inject CSS at runtime. Gui is designed to work this way — the
* compiler is an optimization, not a requirement.
*
* `react-native` is aliased to `react-native-web` for the browser.
*
* The v5 config sets `onlyShorthandStyleProps`, so components use Gui shorthand
* props (p/px/items/justify/...). `tsc --noEmit` passes clean, so the build
* type-checks too (no error suppression).
*/
const __dirname = dirname(fileURLToPath(import.meta.url))
/** Every installed `@hanzogui/*` package, discovered (not hardcoded). */
function guiPackages() {
const dir = join(__dirname, 'node_modules', '@hanzogui')
let scoped = []
try {
scoped = readdirSync(dir).map((name) => `@hanzogui/${name}`)
} catch {
scoped = []
}
return ['@hanzo/gui', '@hanzo/iam-js-sdk', 'react-native-web', ...scoped]
}
/**
* Strict security headers for every response (the ONE place they are set).
*
* CSP closes the high-value vectors: `frame-ancestors 'none'` kills clickjacking
* (+ X-Frame-Options: DENY for legacy UAs), `object-src 'none'` plugins,
* `base-uri 'self'` base-tag hijack, scoped `form-action`/`connect-src` limit
* credential/data exfil to the Hanzo/Lux/Zoo/Pars brand backends, HSTS forces TLS,
* nosniff stops MIME confusion, and Referrer-Policy stops URL leakage.
*
* `script-src` uses `'unsafe-inline'` (NOT a nonce): console2's pages are
* statically prerendered, so Next cannot inject a per-request nonce into the
* static HTML — a nonce+`strict-dynamic` policy blocks every script and
* white-screens the app. Nonce-strict CSP would require forcing dynamic rendering
* app-wide; tracked as a follow-up. console2 renders all data as escaped React
* text (no HTML-injection sink it introduces), so the residual XSS surface is low
* and the remaining directives still contain any exploit.
*/
const CSP = [
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
"form-action 'self' https://hanzo.id https://lux.id https://zoolabs.id https://pars.id",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"font-src 'self' data:",
"connect-src 'self' https://*.hanzo.ai https://*.hanzo.network https://hanzo.id https://*.lux.cloud https://*.lux.network https://lux.id https://*.zoo.cloud https://*.zoo.ngo https://*.zoo.network https://zoolabs.id https://*.pars.cloud https://*.pars.network https://pars.id",
"frame-src 'self'",
"worker-src 'self' blob:",
"manifest-src 'self'",
].join('; ')
const SECURITY_HEADERS = [
{ key: 'Content-Security-Policy', value: CSP },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'no-referrer' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-DNS-Prefetch-Control', value: 'off' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()' },
]
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
/**
* If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
* out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ["en"],
defaultLocale: "en",
// Drop the `X-Powered-By: Next.js` fingerprint (no framework disclosure).
poweredByHeader: false,
transpilePackages: guiPackages(),
experimental: {
esmExternals: true,
},
output: "standalone",
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "x-frame-options",
value: "SAMEORIGIN",
},
],
},
// Required to check authentication status from langfuse.com
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ?
[
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
]
},
] : []
)
]
return [{ source: '/(.*)', headers: SECURITY_HEADERS }]
},
// webassembly support for @dqbd/tiktoken
webpack(config) {
config.experiments = {
asyncWebAssembly: true,
layers: true,
};
return config;
config.resolve.alias = {
...config.resolve.alias,
'react-native$': 'react-native-web',
}
// Gui flags the platform via this define; web build.
config.resolve.extensions = [
'.web.tsx',
'.web.ts',
'.web.jsx',
'.web.js',
...config.resolve.extensions,
]
return config
},
sentry: {
// See the sections below for information on the following options:
// 'Configure Source Maps':
// - disableServerWebpackPlugin
// - disableClientWebpackPlugin
// - hideSourceMaps
hideSourceMaps: true,
// - widenClientFileUpload
// 'Configure Legacy Browser Support':
// - transpileClientSDK
// 'Configure Serverside Auto-instrumentation':
// - autoInstrumentServerFunctions
// - excludeServerRoutes
// 'Configure Tunneling':
// - tunnelRoute
tunnelRoute: "/api/monitoring-tunnel",
},
};
}
const sentryWebpackPluginOptions = {
// Additional config options for the Sentry Webpack plugin. Keep in mind that
// the following options are set automatically, and overriding them is not
// recommended:
// release, url, authToken, configFile, stripPrefix,
// urlPrefix, include, ignore
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
silent: true, // Suppresses all logs
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options.
};
export default withSentryConfig(nextConfig, sentryWebpackPluginOptions);
export default nextConfig
+10799 -15033
View File
File diff suppressed because it is too large Load Diff
+35 -165
View File
@@ -1,176 +1,46 @@
{
"name": "langfuse-core",
"version": "2.4.2",
"name": "@hanzo/console2",
"version": "0.4.2",
"private": true,
"license": "BSD-3-Clause",
"author": "Hanzo AI <dev@hanzo.ai>",
"description": "Hanzo Cloud Console — unified admin console for Hanzo Cloud and all cloud products.",
"scripts": {
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
"dev": "next dev -p 4000",
"build": "next build",
"dev": "next dev",
"dx": "npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
"postinstall": "prisma generate",
"lint": "next lint",
"lint:fix": "next lint --fix",
"prettier": "prettier --write ./src *.{ts,js}",
"clean": "rm -rf node_modules",
"start": "next start",
"test": "jest --runInBand",
"test:watch": "jest --watch --runInBand",
"start": "next start -p 4000",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:unit": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"infra:dev:up": "docker-compose -f docker-compose.dev.yml up -d",
"infra:dev:down": "docker-compose -f docker-compose.dev.yml down",
"db:migrate": "DISABLE_ERD=false npx prisma migrate dev",
"db:reset": "npx prisma migrate reset",
"db:seed": "npx prisma db seed",
"db:seed:examples": "npx prisma db seed -- --environment examples",
"release": "release-it",
"models:migrate": "tsx scripts/model-match.ts"
},
"prisma": {
"seed": "ts-node -r tsconfig-paths/register --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
"test:all": "vitest run && playwright test"
},
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@aws-sdk/client-s3": "^3.507.0",
"@aws-sdk/lib-storage": "^3.511.0",
"@aws-sdk/s3-request-presigner": "^3.507.0",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.9.1",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.14",
"@react-email/render": "^0.0.12",
"@sentry/nextjs": "^7.100.1",
"@sentry/profiling-node": "^7.100.1",
"@sentry/types": "^7.88.0",
"@t3-oss/env-nextjs": "^0.8.0",
"@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.8",
"@tremor/react": "^3.11.1",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.1",
"core-js": "^3.35.1",
"cors": "^2.8.5",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"js-tiktoken": "^1.0.10",
"lodash": "^4.17.21",
"lucide-react": "^0.330.0",
"next": "^14.1.0",
"next-auth": "^4.24.5",
"next-query-params": "^5.0.0",
"nodemailer": "^6.9.9",
"posthog-js": "^1.105.7",
"posthog-node": "^3.6.2",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"react-responsive": "^9.0.2",
"react18-json-view": "^0.2.7",
"sonner": "^1.4.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"zod": "^3.22.4"
"@hanzo/gui": "7.3.0",
"@hanzo/iam-js-sdk": "0.19.1",
"@zap-proto/web": "1.0.0",
"@zap-proto/zap": "1.6.0",
"superjson": "2.2.2",
"@hanzogui/config": "7.3.0",
"@hanzogui/core": "7.3.0",
"@hanzogui/lucide-icons-2": "7.3.0",
"@hanzogui/next-theme": "7.3.0",
"ethers": "6.17.0",
"next": "15.5.19",
"react": "19.2.7",
"react-dom": "19.2.7",
"react-native-web": "0.21.2"
},
"devDependencies": {
"@jedmao/location": "^3.0.0",
"@mermaid-js/mermaid-cli": "^10.7.0",
"@playwright/test": "^1.41.2",
"@release-it/bumper": "^6.0.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/eslint": "^8.56.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.17",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"prisma": "^5.9.1",
"prisma-erd-generator": "^1.11.2",
"release-it": "^17.0.3",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.7.1",
"typescript": "^5.3.3"
},
"ct3aMetadata": {
"initVersion": "7.13.0"
},
"optionalDependencies": {
"crisp-sdk-web": "^1.0.21"
},
"release-it": {
"git": {
"commitMessage": "chore: release v${version}",
"tagName": "v${version}"
},
"github": {
"release": true,
"web": true,
"autoGenerate": true,
"releaseName": "v${version}",
"comments": {
"submit": true,
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
}
},
"plugins": {
"@release-it/bumper": {
"out": {
"file": "./src/constants/VERSION.ts",
"type": "application/typescript"
}
}
}
"@playwright/test": "1.61.1",
"@types/node": "22.20.0",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitest/coverage-v8": "3.2.6",
"jsdom": "29.1.1",
"react-native": "0.83.9",
"typescript": "5.9.3",
"vitest": "3.2.6"
}
}
+33 -8
View File
@@ -1,15 +1,40 @@
import { defineConfig } from "@playwright/test";
import { defineConfig, devices } from '@playwright/test'
/**
* E2E config for console2.
*
* Tests are HERMETIC: the cloud `/v1` backend and the `/paas` proxy are mocked
* with Playwright route interception (see test/e2e/fixtures.ts), so the suite is
* deterministic and never touches real prod data. The webServer builds and
* serves the REAL Next app, so every assertion is against real rendered UI and
* real client logic.
*
* Run: `npm run test:e2e` (one command; builds + serves + tests).
*/
const PORT = 4000
const BASE_URL = `http://localhost:${PORT}`
export default defineConfig({
testDir: './test/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : [['list']],
timeout: 45_000,
expect: { timeout: 10_000 },
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000",
baseURL: BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
locale: 'en-US',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: process.env.CI ? "npm run start" : "npm run dev",
url: "http://127.0.0.1:3000",
command: 'npm run build && npm run start',
url: BASE_URL,
timeout: 300_000,
reuseExistingServer: !process.env.CI,
stdout: "ignore",
stderr: "pipe",
env: { NODE_OPTIONS: '--max-old-space-size=6144' },
},
});
})
-8
View File
@@ -1,8 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = config;
-6
View File
@@ -1,6 +0,0 @@
/** @type {import("prettier").Config} */
const config = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};
module.exports = config;
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 194 KiB

@@ -1,75 +0,0 @@
-- CreateTable
CREATE TABLE "Example" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Example_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,30 +0,0 @@
-- CreateTable
CREATE TABLE "traces" (
"id" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"attributes" JSONB NOT NULL,
"status" TEXT NOT NULL,
"status_message" TEXT,
CONSTRAINT "traces_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "observations" (
"id" TEXT NOT NULL,
"traceId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"start_time" TIMESTAMP(3) NOT NULL,
"end_time" TIMESTAMP(3) NOT NULL,
"attributes" JSONB NOT NULL,
"parentObservationId" TEXT,
CONSTRAINT "observations_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "observations" ADD CONSTRAINT "observations_traceId_fkey" FOREIGN KEY ("traceId") REFERENCES "traces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "observations" ADD CONSTRAINT "observations_parentObservationId_fkey" FOREIGN KEY ("parentObservationId") REFERENCES "observations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,17 +0,0 @@
/*
Warnings:
- You are about to drop the column `parentObservationId` on the `observations` table. All the data in the column will be lost.
- Added the required column `type` to the `observations` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "observations" DROP CONSTRAINT "observations_parentObservationId_fkey";
-- AlterTable
ALTER TABLE "observations" DROP COLUMN "parentObservationId",
ADD COLUMN "parent_observation_id" TEXT,
ADD COLUMN "type" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "observations" ADD CONSTRAINT "observations_parent_observation_id_fkey" FOREIGN KEY ("parent_observation_id") REFERENCES "observations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,30 +0,0 @@
/*
Warnings:
- Changed the type of `type` on the `observations` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- CreateEnum
CREATE TYPE "ObservationType" AS ENUM ('SPAN', 'EVENT', 'LLMCALL');
-- AlterTable
ALTER TABLE "observations" DROP COLUMN "type",
ADD COLUMN "type" "ObservationType" NOT NULL;
-- CreateTable
CREATE TABLE "metrics" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"value" INTEGER NOT NULL,
"traceId" TEXT NOT NULL,
"observationId" TEXT,
CONSTRAINT "metrics_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "metrics" ADD CONSTRAINT "metrics_traceId_fkey" FOREIGN KEY ("traceId") REFERENCES "traces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "metrics" ADD CONSTRAINT "metrics_observationId_fkey" FOREIGN KEY ("observationId") REFERENCES "observations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "observations" ALTER COLUMN "end_time" DROP NOT NULL;
@@ -1,15 +0,0 @@
/*
Warnings:
- You are about to drop the column `createdAt` on the `metrics` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "metrics" DROP COLUMN "createdAt",
ADD COLUMN "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "observations" ALTER COLUMN "start_time" SET DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "traces" ALTER COLUMN "timestamp" SET DEFAULT CURRENT_TIMESTAMP;
@@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `metrics` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "metrics" DROP CONSTRAINT "metrics_observationId_fkey";
-- DropForeignKey
ALTER TABLE "metrics" DROP CONSTRAINT "metrics_traceId_fkey";
-- DropTable
DROP TABLE "metrics";
-- CreateTable
CREATE TABLE "gradings" (
"id" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"value" INTEGER NOT NULL,
"traceId" TEXT NOT NULL,
"observationId" TEXT,
CONSTRAINT "gradings_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "gradings" ADD CONSTRAINT "gradings_traceId_fkey" FOREIGN KEY ("traceId") REFERENCES "traces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "gradings" ADD CONSTRAINT "gradings_observationId_fkey" FOREIGN KEY ("observationId") REFERENCES "observations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `gradings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "gradings" DROP CONSTRAINT "gradings_observationId_fkey";
-- DropForeignKey
ALTER TABLE "gradings" DROP CONSTRAINT "gradings_traceId_fkey";
-- DropTable
DROP TABLE "gradings";
-- CreateTable
CREATE TABLE "scores" (
"id" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"value" INTEGER NOT NULL,
"traceId" TEXT NOT NULL,
"observationId" TEXT,
CONSTRAINT "scores_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_traceId_fkey" FOREIGN KEY ("traceId") REFERENCES "traces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_observationId_fkey" FOREIGN KEY ("observationId") REFERENCES "observations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;
@@ -1,178 +0,0 @@
/*
Warnings:
- You are about to drop the column `userId` on the `Account` table. All the data in the column will be lost.
- You are about to drop the column `createdAt` on the `Example` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `Example` table. All the data in the column will be lost.
- You are about to drop the column `sessionToken` on the `Session` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `Session` table. All the data in the column will be lost.
- You are about to drop the column `traceId` on the `observations` table. All the data in the column will be lost.
- You are about to drop the column `observationId` on the `scores` table. All the data in the column will be lost.
- You are about to drop the column `traceId` on the `scores` table. All the data in the column will be lost.
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `VerificationToken` table. If the table is not empty, all the data it contains will be lost.
- A unique constraint covering the columns `[session_token]` on the table `Session` will be added. If there are existing duplicate values, this will fail.
- Added the required column `user_id` to the `Account` table without a default value. This is not possible if the table is not empty.
- Added the required column `updated_at` to the `Example` table without a default value. This is not possible if the table is not empty.
- Added the required column `session_token` to the `Session` table without a default value. This is not possible if the table is not empty.
- Added the required column `user_id` to the `Session` table without a default value. This is not possible if the table is not empty.
- Added the required column `trace_id` to the `observations` table without a default value. This is not possible if the table is not empty.
- Added the required column `trace_id` to the `scores` table without a default value. This is not possible if the table is not empty.
- Added the required column `project_id` to the `traces` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "MembershipRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
-- DropForeignKey
ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey";
-- DropForeignKey
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";
-- DropForeignKey
ALTER TABLE "observations" DROP CONSTRAINT "observations_traceId_fkey";
-- DropForeignKey
ALTER TABLE "scores" DROP CONSTRAINT "scores_observationId_fkey";
-- DropForeignKey
ALTER TABLE "scores" DROP CONSTRAINT "scores_traceId_fkey";
-- DropIndex
DROP INDEX "Session_sessionToken_key";
-- AlterTable
ALTER TABLE "Account" DROP COLUMN "userId",
ADD COLUMN "user_id" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Example" DROP COLUMN "createdAt",
DROP COLUMN "updatedAt",
ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL;
-- AlterTable
ALTER TABLE "Session" DROP COLUMN "sessionToken",
DROP COLUMN "userId",
ADD COLUMN "session_token" TEXT NOT NULL,
ADD COLUMN "user_id" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "observations" DROP COLUMN "traceId",
ADD COLUMN "trace_id" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "scores" DROP COLUMN "observationId",
DROP COLUMN "traceId",
ADD COLUMN "observation_id" TEXT,
ADD COLUMN "trace_id" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "traces" ADD COLUMN "project_id" TEXT NOT NULL;
-- DropTable
DROP TABLE "User";
-- DropTable
DROP TABLE "VerificationToken";
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"password" TEXT,
"image" TEXT,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification_tokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "projects" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "api_keys" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"note" TEXT,
"publishable_key" TEXT NOT NULL,
"hashed_secret_key" TEXT NOT NULL,
"display_secret_key" TEXT NOT NULL,
"last_used_at" TIMESTAMP(3),
"expires_at" TIMESTAMP(3),
"project_id" TEXT NOT NULL,
CONSTRAINT "api_keys_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "memberships" (
"project_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"role" "MembershipRole" NOT NULL,
CONSTRAINT "memberships_pkey" PRIMARY KEY ("project_id","user_id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token");
-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "api_keys_id_key" ON "api_keys"("id");
-- CreateIndex
CREATE UNIQUE INDEX "api_keys_publishable_key_key" ON "api_keys"("publishable_key");
-- CreateIndex
CREATE UNIQUE INDEX "api_keys_hashed_secret_key_key" ON "api_keys"("hashed_secret_key");
-- CreateIndex
CREATE UNIQUE INDEX "Session_session_token_key" ON "Session"("session_token");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "memberships" ADD CONSTRAINT "memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "traces" ADD CONSTRAINT "traces_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "observations" ADD CONSTRAINT "observations_trace_id_fkey" FOREIGN KEY ("trace_id") REFERENCES "traces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_trace_id_fkey" FOREIGN KEY ("trace_id") REFERENCES "traces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_observation_id_fkey" FOREIGN KEY ("observation_id") REFERENCES "observations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -1,10 +0,0 @@
/*
Warnings:
- You are about to drop the column `status` on the `traces` table. All the data in the column will be lost.
- You are about to drop the column `status_message` on the `traces` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "traces" DROP COLUMN "status",
DROP COLUMN "status_message";
@@ -1,28 +0,0 @@
/*
Warnings:
- THIS IS BREAKING
*/
-- AlterEnum
BEGIN;
CREATE TYPE "ObservationType_new" AS ENUM ('SPAN', 'EVENT', 'GENERATION');
ALTER TABLE "observations" ALTER COLUMN "type" TYPE "ObservationType_new" USING ("type"::text::"ObservationType_new");
ALTER TYPE "ObservationType" RENAME TO "ObservationType_old";
ALTER TYPE "ObservationType_new" RENAME TO "ObservationType";
DROP TYPE "ObservationType_old";
COMMIT;
-- AlterTable
ALTER TABLE "observations" DROP COLUMN "attributes",
ADD COLUMN "completion" TEXT,
ADD COLUMN "metadata" JSONB,
ADD COLUMN "model" TEXT,
ADD COLUMN "modelParameters" JSONB,
ADD COLUMN "prompt" JSONB,
ADD COLUMN "usage" JSONB,
ALTER COLUMN "name" DROP NOT NULL;
-- AlterTable
ALTER TABLE "traces" DROP COLUMN "attributes",
ADD COLUMN "metadata" JSONB,
ALTER COLUMN "name" DROP NOT NULL;
@@ -1,18 +0,0 @@
BEGIN;
ALTER TABLE "observations"
RENAME COLUMN "prompt" TO "input";
ALTER TABLE "observations"
ADD COLUMN "output_temp" JSONB;
UPDATE "observations"
SET "output_temp" = json_build_object('completion', "observations"."completion");
ALTER TABLE "observations"
DROP COLUMN "completion";
ALTER TABLE "observations"
RENAME COLUMN "output_temp" TO "output";
COMMIT;

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