Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce37139741 | ||
|
|
e3e42c279f | ||
|
|
345015b745 | ||
|
|
a318a23065 | ||
|
|
e3c4d52bb4 | ||
|
|
da83b67e89 | ||
|
|
a13de116e8 | ||
|
|
725beb1d98 | ||
|
|
21c1016292 | ||
|
|
bc76e6fc76 | ||
|
|
24f9b14e69 | ||
|
|
ac0ecfcb84 | ||
|
|
e547011c96 | ||
|
|
164f073b50 | ||
|
|
6df405adcd | ||
|
|
f50dbdd9a6 | ||
|
|
b4274bda4a | ||
|
|
c9adc16cad | ||
|
|
f30cdbbee5 | ||
|
|
a307d64b07 | ||
|
|
c32bb30154 | ||
|
|
c27a62d271 | ||
|
|
102a37a48c | ||
|
|
6beb670744 | ||
|
|
82e81ddf75 | ||
|
|
1ad8ff3fa0 | ||
|
|
631dc417b2 | ||
|
|
57371b1c9d | ||
|
|
0076f411b0 | ||
|
|
9b04ea848c | ||
|
|
e30e9c7b3e | ||
|
|
f057352fc3 | ||
|
|
a0db49f282 | ||
|
|
c09943eb08 | ||
|
|
0e2ee1f394 | ||
|
|
c6f55d8646 | ||
|
|
14519c41c5 | ||
|
|
1f80429f97 | ||
|
|
800b3481d7 | ||
|
|
7e3fa15425 | ||
|
|
07db3af7fe | ||
|
|
85e68e433e | ||
|
|
5527bb30dd | ||
|
|
1b03d838db | ||
|
|
42f591214c | ||
|
|
668f6b8951 | ||
|
|
7895e104b0 | ||
|
|
bfd2a2dd37 | ||
|
|
cf5d3094ae | ||
|
|
7a3dd59128 | ||
|
|
f117a01fac | ||
|
|
2b4d64c9cf |
@@ -1,4 +0,0 @@
|
||||
[codespell]
|
||||
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma
|
||||
ignore-words-list = afterall,vertx
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 can’t get something to work the way you expect, open a question in our discussion forums.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"Prisma.prisma"
|
||||
]
|
||||
}
|
||||
Vendored
-28
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-33
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
-218
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)}¤cy=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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 />
|
||||
}
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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` |
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.16.36"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
python/
|
||||
@@ -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
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+10799
-15033
File diff suppressed because it is too large
Load Diff
+35
-165
@@ -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
@@ -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' },
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -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
Reference in New Issue
Block a user