diff --git a/.github/workflows/ai_cd.yaml b/.github/workflows/ai_cd.yaml deleted file mode 100644 index ea88252564..0000000000 --- a/.github/workflows/ai_cd.yaml +++ /dev/null @@ -1,45 +0,0 @@ -on: - workflow_dispatch: - -jobs: - compute-version: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - - run: git fetch --tags --force - - uses: ./.github/actions/doxxer_install - - id: version - run: | - VERSION=$(doxxer --config doxxer.ai.toml next patch) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Computed version: $VERSION" - - deploy: - needs: compute-version - runs-on: ubuntu-latest - timeout-minutes: 60 - concurrency: ai-fly-deploy - steps: - - uses: actions/checkout@v4 - - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl deploy --config apps/ai/fly.toml --dockerfile apps/ai/Dockerfile --remote-only --build-arg APP_VERSION=${{ needs.compute-version.outputs.version }} - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - tag: - needs: [compute-version, deploy] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - uses: mathieudutour/github-tag-action@v6.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: ai_v${{ needs.compute-version.outputs.version }} - tag_prefix: "" diff --git a/.github/workflows/ai_ci.yaml b/.github/workflows/ai_ci.yaml deleted file mode 100644 index e2a4ae73c9..0000000000 --- a/.github/workflows/ai_ci.yaml +++ /dev/null @@ -1,38 +0,0 @@ -on: - workflow_dispatch: - push: - branches: - - main - paths: - - apps/ai/** - - crates/llm-proxy/** - - crates/transcribe-proxy/** - pull_request: - branches-ignore: - - "**/graphite-base/**" - paths: - - apps/ai/** - - crates/llm-proxy/** - - crates/transcribe-proxy/** -jobs: - optimize_ci: - runs-on: ubuntu-latest - outputs: - skip: ${{ steps.check_skip.outputs.skip }} - steps: - - uses: actions/checkout@v4 - - id: check_skip - uses: ./.github/actions/graphite_optimizer - with: - graphite_token: ${{ secrets.GRAPHITE_TOKEN }} - - ci: - needs: optimize_ci - if: needs.optimize_ci.outputs.skip == 'false' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/rust_install - with: - platform: linux - - run: cargo check -p ai diff --git a/.github/workflows/api_cd.yaml b/.github/workflows/api_cd.yaml index 269136d21e..dfe5c57b36 100644 --- a/.github/workflows/api_cd.yaml +++ b/.github/workflows/api_cd.yaml @@ -21,13 +21,13 @@ jobs: deploy: needs: compute-version - runs-on: depot-ubuntu-24.04-8 + runs-on: ubuntu-latest timeout-minutes: 60 concurrency: api-fly-deploy steps: - uses: actions/checkout@v4 - uses: superfly/flyctl-actions/setup-flyctl@master - - run: flyctl deploy --config apps/api/fly.toml --dockerfile apps/api/Dockerfile --remote-only -e APP_VERSION=${{ needs.compute-version.outputs.version }} + - run: flyctl deploy --config apps/api/fly.ai.toml --dockerfile apps/api/Dockerfile --remote-only --build-arg APP_VERSION=${{ needs.compute-version.outputs.version }} env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.github/workflows/api_ci.yaml b/.github/workflows/api_ci.yaml index de01e14edd..cfc320c770 100644 --- a/.github/workflows/api_ci.yaml +++ b/.github/workflows/api_ci.yaml @@ -5,11 +5,15 @@ on: - main paths: - apps/api/** + - crates/llm-proxy/** + - crates/transcribe-proxy/** pull_request: branches-ignore: - "**/graphite-base/**" paths: - apps/api/** + - crates/llm-proxy/** + - crates/transcribe-proxy/** jobs: optimize_ci: runs-on: ubuntu-latest @@ -28,13 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/pnpm_install - - uses: ./.github/actions/bun_install - - run: pnpm -F @hypr/api typecheck - - name: Run tests if they exist - run: | - if find apps/api/src -name "*.test.ts" -o -name "*.spec.ts" | grep -q .; then - pnpm -F @hypr/api test - else - echo "No test files found, skipping tests" - fi + - uses: ./.github/actions/rust_install + with: + platform: linux + - run: cargo check -p api diff --git a/.github/workflows/k6.yaml b/.github/workflows/k6.yaml deleted file mode 100644 index fcb20bd357..0000000000 --- a/.github/workflows/k6.yaml +++ /dev/null @@ -1,49 +0,0 @@ -on: - workflow_dispatch: - inputs: - test_duration_minutes: - description: "Test duration in minutes" - default: "60" - vus: - description: "Number of virtual users" - default: "30" - -jobs: - load-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: superfly/flyctl-actions/setup-flyctl@master - - - run: flyctl deploy --config apps/api/fly.toml --app hyprnote-api-loadtest --strategy immediate --wait-timeout 120 --env LOAD_TEST=true - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - - run: flyctl scale count 2 --app hyprnote-api-loadtest --yes - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - - uses: grafana/setup-k6-action@v1 - - - run: k6 run apps/k6/scripts/stt-live.js - env: - API_URL: wss://hyprnote-api-loadtest.fly.dev - AUTH_TOKEN: ${{ secrets.K6_AUTH_TOKEN }} - TEST_DURATION: ${{ inputs.test_duration_minutes }}m - VUS: ${{ inputs.vus }} - FLY_ORG: ${{ secrets.FLY_ORG }} - FLY_APP: hyprnote-api-loadtest - FLY_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - - uses: actions/upload-artifact@v4 - if: always() - with: - name: k6-report-${{ github.run_id }} - path: stt-live-*.json - retention-days: 90 - - - run: flyctl scale count 0 --app hyprnote-api-loadtest --yes - if: always() - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.github/workflows/stripe_backfill.yaml b/.github/workflows/stripe_backfill.yaml deleted file mode 100644 index 6197c845cb..0000000000 --- a/.github/workflows/stripe_backfill.yaml +++ /dev/null @@ -1,34 +0,0 @@ -on: - workflow_dispatch: - inputs: - created_gte: - description: "Only sync objects created at or after this unix timestamp (optional)" - required: false - type: string - created_lte: - description: "Only sync objects created at or before this unix timestamp (optional)" - required: false - type: string - -jobs: - backfill: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bun_install - - uses: ./.github/actions/pnpm_install - - uses: ./.github/actions/infisical_install - - run: pnpm --filter @hypr/api install - - run: | - ARGS="" - if [ -n "${{ inputs.created_gte }}" ]; then - ARGS="$ARGS --created-gte ${{ inputs.created_gte }}" - fi - if [ -n "${{ inputs.created_lte }}" ]; then - ARGS="$ARGS --created-lte ${{ inputs.created_lte }}" - fi - infisical run --token="$INFISICAL_TOKEN" --env=prod --projectId="$INFISICAL_PROJECT_ID" --path="/stripe-backfill" -- bun apps/api/src/scripts/stripe-backfill.ts $ARGS - env: - INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }} - INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }} diff --git a/Cargo.lock b/Cargo.lock index c0459ddd32..f006985ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,39 +243,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ai" -version = "0.1.0" -dependencies = [ - "analytics", - "api-auth", - "api-calendar", - "api-env", - "api-nango", - "api-subscription", - "api-support", - "axum 0.8.8", - "dotenvy", - "envy", - "jsonwebtoken", - "llm-proxy", - "owhisper-client", - "reqwest 0.13.1", - "rustls 0.23.36", - "sentry", - "serde", - "serde_json", - "tokio", - "tower 0.5.3", - "tower-http 0.6.8", - "tracing", - "tracing-subscriber", - "transcribe-proxy", - "url", - "utoipa", - "vergen-gix", -] - [[package]] name = "aligned" version = "0.4.3" @@ -535,6 +502,39 @@ dependencies = [ "tower 0.5.3", ] +[[package]] +name = "api" +version = "0.1.0" +dependencies = [ + "analytics", + "api-auth", + "api-calendar", + "api-env", + "api-nango", + "api-subscription", + "api-support", + "axum 0.8.8", + "dotenvy", + "envy", + "jsonwebtoken", + "llm-proxy", + "owhisper-client", + "reqwest 0.13.1", + "rustls 0.23.36", + "sentry", + "serde", + "serde_json", + "tokio", + "tower 0.5.3", + "tower-http 0.6.8", + "tracing", + "tracing-subscriber", + "transcribe-proxy", + "url", + "utoipa", + "vergen-gix", +] + [[package]] name = "api-auth" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 199f534268..b8e85c9e8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ debug = false [workspace] resolver = "2" members = [ - "apps/ai", + "apps/api", "apps/desktop/src-tauri", "apps/eval-cli", "apps/granola", diff --git a/Taskfile.yaml b/Taskfile.yaml index 46778b144d..f1667e2308 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -3,8 +3,6 @@ dotenv: [".env"] includes: web: taskfile: ./apps/web - api: - taskfile: ./apps/api tasks: py:init: POETRY_VIRTUALENVS_IN_PROJECT=true poetry install --no-cache --no-interaction --all-extras diff --git a/apps/ai/AGENTS.md b/apps/ai/AGENTS.md deleted file mode 100644 index 0601b2963e..0000000000 --- a/apps/ai/AGENTS.md +++ /dev/null @@ -1,10 +0,0 @@ -```bash -# REPO="SOMEWHERE" ($HOME/repos/hyprnote inside Devin) -infisical export \ - --env=dev \ - --secret-overriding=false \ - --format=dotenv \ - --output-file="$REPO/apps/ai/.env" \ - --projectId=87dad7b5-72a6-4791-9228-b3b86b169db1 \ - --path="/ai" -``` diff --git a/apps/ai/Dockerfile b/apps/ai/Dockerfile deleted file mode 100644 index 1d3e748b44..0000000000 --- a/apps/ai/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG RUST_VERSION=1.93.0 - -FROM rust:${RUST_VERSION}-bookworm AS planner -RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* -RUN cargo install cargo-chef --locked -WORKDIR /app -COPY Cargo.toml Cargo.lock ./ -RUN sed -i '/^members = \[/,/^\]/c\members = ["apps/ai", "crates/*"]' Cargo.toml -COPY crates crates -COPY apps/ai/Cargo.toml apps/ai/Cargo.toml -RUN mkdir -p apps/ai/src && echo "fn main() {}" > apps/ai/src/main.rs -RUN cargo chef prepare --recipe-path recipe.json - -FROM rust:${RUST_VERSION}-bookworm AS build -ARG APP_VERSION -RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* -RUN cargo install cargo-chef sccache --locked -ENV RUSTC_WRAPPER=sccache \ - SCCACHE_DIR=/sccache \ - APP_VERSION=${APP_VERSION} -WORKDIR /app -COPY --from=planner /app/recipe.json recipe.json -RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ - --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ - cargo chef cook --release --recipe-path recipe.json -p ai -COPY Cargo.toml Cargo.lock ./ -RUN sed -i '/^members = \[/,/^\]/c\members = ["apps/ai", "crates/*"]' Cargo.toml -COPY crates crates -COPY apps/ai apps/ai -RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ - --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ - cargo build --release -p ai - -FROM debian:bookworm-slim AS runtime -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* -RUN groupadd -g 1001 appgroup && \ - useradd -u 1001 -g appgroup -d /nonexistent -s /usr/sbin/nologin -M appuser -COPY --from=build --chown=appuser:appgroup /app/target/release/ai /usr/local/bin/ai -USER appuser -WORKDIR /app -EXPOSE 3001 -ENTRYPOINT ["/usr/local/bin/ai"] diff --git a/apps/ai/openapi.gen.json b/apps/ai/openapi.gen.json deleted file mode 100644 index c85701e5cd..0000000000 --- a/apps/ai/openapi.gen.json +++ /dev/null @@ -1,700 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "Hyprnote AI API", - "description": "AI services API for speech-to-text transcription, LLM chat completions, and subscription management", - "license": { - "name": "" - }, - "version": "1.0.0" - }, - "paths": { - "/calendar/calendars": { - "post": { - "tags": [ - "calendar" - ], - "operationId": "list_calendars", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListCalendarsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Calendars fetched", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListCalendarsResponse" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/calendar/events": { - "post": { - "tags": [ - "calendar" - ], - "operationId": "list_events", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListEventsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Events fetched", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListEventsResponse" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/calendar/events/create": { - "post": { - "tags": [ - "calendar" - ], - "operationId": "create_event", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEventRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Event created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEventResponse" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/nango/connect-session": { - "post": { - "tags": [ - "nango" - ], - "operationId": "create_connect_session", - "responses": { - "200": { - "description": "Connect session created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConnectSessionResponse" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/nango/webhook": { - "post": { - "tags": [ - "nango" - ], - "operationId": "nango_webhook", - "responses": { - "200": { - "description": "Webhook processed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WebhookResponse" - } - } - } - }, - "400": { - "description": "Bad request" - }, - "401": { - "description": "Invalid signature" - } - } - } - }, - "/subscription/can-start-trial": { - "get": { - "tags": [ - "subscription" - ], - "operationId": "can_start_trial", - "responses": { - "200": { - "description": "Check successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CanStartTrialResponse" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/subscription/start-trial": { - "post": { - "tags": [ - "subscription" - ], - "operationId": "start_trial", - "parameters": [ - { - "name": "interval", - "in": "query", - "required": false, - "schema": { - "$ref": "#/components/schemas/Interval" - }, - "example": "monthly" - } - ], - "responses": { - "200": { - "description": "Trial started successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StartTrialResponse" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "500": { - "description": "Internal server error" - } - }, - "security": [ - { - "bearer_auth": [] - } - ] - } - }, - "/feedback/submit": { - "post": { - "tags": [ - "feedback" - ], - "operationId": "submit", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FeedbackRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Feedback submitted successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FeedbackResponse" - } - } - } - }, - "400": { - "description": "Invalid request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FeedbackResponse" - } - } - } - }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FeedbackResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "CanStartTrialResponse": { - "type": "object", - "required": [ - "canStartTrial" - ], - "properties": { - "canStartTrial": { - "type": "boolean", - "example": true - } - } - }, - "ConnectSessionResponse": { - "type": "object", - "required": [ - "token", - "expires_at" - ], - "properties": { - "expires_at": { - "type": "string" - }, - "token": { - "type": "string" - } - } - }, - "CreateEventRequest": { - "type": "object", - "required": [ - "connection_id", - "calendar_id", - "summary", - "start", - "end" - ], - "properties": { - "attendees": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/EventAttendee" - } - }, - "calendar_id": { - "type": "string" - }, - "connection_id": { - "type": "string" - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "end": { - "$ref": "#/components/schemas/EventDateTime" - }, - "location": { - "type": [ - "string", - "null" - ] - }, - "start": { - "$ref": "#/components/schemas/EventDateTime" - }, - "summary": { - "type": "string" - } - } - }, - "CreateEventResponse": { - "type": "object", - "required": [ - "event" - ], - "properties": { - "event": {} - } - }, - "DeviceInfo": { - "type": "object", - "required": [ - "platform", - "arch", - "osVersion", - "appVersion" - ], - "properties": { - "appVersion": { - "type": "string" - }, - "arch": { - "type": "string" - }, - "osVersion": { - "type": "string" - }, - "platform": { - "type": "string" - } - } - }, - "EventAttendee": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "displayName": { - "type": [ - "string", - "null" - ] - }, - "email": { - "type": "string" - }, - "optional": { - "type": [ - "boolean", - "null" - ] - } - } - }, - "EventDateTime": { - "type": "object", - "properties": { - "date": { - "type": [ - "string", - "null" - ] - }, - "dateTime": { - "type": [ - "string", - "null" - ] - }, - "timeZone": { - "type": [ - "string", - "null" - ] - } - } - }, - "FeedbackRequest": { - "type": "object", - "required": [ - "type", - "description", - "deviceInfo" - ], - "properties": { - "description": { - "type": "string" - }, - "deviceInfo": { - "$ref": "#/components/schemas/DeviceInfo" - }, - "logs": { - "type": [ - "string", - "null" - ] - }, - "type": { - "$ref": "#/components/schemas/FeedbackType" - } - } - }, - "FeedbackResponse": { - "type": "object", - "required": [ - "success" - ], - "properties": { - "error": { - "type": [ - "string", - "null" - ] - }, - "issueUrl": { - "type": [ - "string", - "null" - ] - }, - "success": { - "type": "boolean" - } - } - }, - "FeedbackType": { - "type": "string", - "enum": [ - "bug", - "feature" - ] - }, - "Interval": { - "type": "string", - "enum": [ - "monthly", - "yearly" - ] - }, - "ListCalendarsRequest": { - "type": "object", - "required": [ - "connection_id" - ], - "properties": { - "connection_id": { - "type": "string" - } - } - }, - "ListCalendarsResponse": { - "type": "object", - "required": [ - "calendars" - ], - "properties": { - "calendars": { - "type": "array", - "items": {} - } - } - }, - "ListEventsRequest": { - "type": "object", - "required": [ - "connection_id", - "calendar_id" - ], - "properties": { - "calendar_id": { - "type": "string" - }, - "connection_id": { - "type": "string" - }, - "max_results": { - "type": [ - "integer", - "null" - ], - "format": "int32", - "minimum": 0 - }, - "order_by": { - "type": [ - "string", - "null" - ] - }, - "page_token": { - "type": [ - "string", - "null" - ] - }, - "single_events": { - "type": [ - "boolean", - "null" - ] - }, - "time_max": { - "type": [ - "string", - "null" - ] - }, - "time_min": { - "type": [ - "string", - "null" - ] - } - } - }, - "ListEventsResponse": { - "type": "object", - "required": [ - "events" - ], - "properties": { - "events": { - "type": "array", - "items": {} - }, - "next_page_token": { - "type": [ - "string", - "null" - ] - } - } - }, - "StartTrialResponse": { - "type": "object", - "required": [ - "started" - ], - "properties": { - "started": { - "type": "boolean", - "example": true - } - } - }, - "WebhookResponse": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string" - } - } - } - }, - "securitySchemes": { - "bearer_auth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "Supabase JWT token" - }, - "device_fingerprint": { - "type": "apiKey", - "in": "header", - "name": "x-device-fingerprint", - "description": "Optional device fingerprint for analytics" - } - } - }, - "tags": [ - { - "name": "stt", - "description": "Speech-to-text transcription endpoints" - }, - { - "name": "llm", - "description": "LLM chat completions endpoints" - }, - { - "name": "calendar", - "description": "Calendar management" - }, - { - "name": "nango", - "description": "Integration management via Nango" - }, - { - "name": "subscription", - "description": "Subscription and trial management" - }, - { - "name": "transcribe", - "description": "Speech-to-text transcription proxy" - }, - { - "name": "llm", - "description": "LLM chat completions proxy" - }, - { - "name": "support", - "description": "User feedback and support" - } - ] -} \ No newline at end of file diff --git a/apps/api/.gitignore b/apps/api/.gitignore deleted file mode 100644 index 5cc92b5ad8..0000000000 --- a/apps/api/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Generated OpenAPI spec (build artifact) -openapi.yaml diff --git a/apps/api/AGENTS.md b/apps/api/AGENTS.md index 7d639a827e..0f6fe28c2c 100644 --- a/apps/api/AGENTS.md +++ b/apps/api/AGENTS.md @@ -6,5 +6,5 @@ infisical export \ --format=dotenv \ --output-file="$REPO/apps/api/.env" \ --projectId=87dad7b5-72a6-4791-9228-b3b86b169db1 \ - --path="/api" + --path="/ai" ``` diff --git a/apps/ai/Cargo.toml b/apps/api/Cargo.toml similarity index 98% rename from apps/ai/Cargo.toml rename to apps/api/Cargo.toml index 5ee696ba94..b049b480a3 100644 --- a/apps/ai/Cargo.toml +++ b/apps/api/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ai" +name = "api" version = "0.1.0" edition = "2024" diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 934e3c0c44..b9a24438bb 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,44 +1,46 @@ -# https://pnpm.io/docker#example-3-build-on-cicd -FROM node:22-slim AS node-base -WORKDIR /repo +# syntax=docker/dockerfile:1 -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -ENV CI=true -RUN corepack enable +ARG RUST_VERSION=1.93.0 -FROM node-base AS deps -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY apps/api/package.json apps/api/package.json -COPY packages/api-client/package.json packages/api-client/package.json -COPY packages/supabase/package.json packages/supabase/package.json -RUN pnpm fetch --prod --filter @hypr/api - -FROM deps AS build +FROM rust:${RUST_VERSION}-bookworm AS planner +RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* +RUN cargo install cargo-chef --locked +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +RUN sed -i '/^members = \[/,/^\]/c\members = ["apps/api", "crates/*"]' Cargo.toml +COPY crates crates +COPY apps/api/Cargo.toml apps/api/Cargo.toml +RUN mkdir -p apps/api/src && echo "fn main() {}" > apps/api/src/main.rs +RUN cargo chef prepare --recipe-path recipe.json + +FROM rust:${RUST_VERSION}-bookworm AS build +ARG APP_VERSION +RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* +RUN cargo install cargo-chef sccache --locked +ENV RUSTC_WRAPPER=sccache \ + SCCACHE_DIR=/sccache \ + APP_VERSION=${APP_VERSION} +WORKDIR /app +COPY --from=planner /app/recipe.json recipe.json +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ + cargo chef cook --release --recipe-path recipe.json -p api +COPY Cargo.toml Cargo.lock ./ +RUN sed -i '/^members = \[/,/^\]/c\members = ["apps/api", "crates/*"]' Cargo.toml +COPY crates crates COPY apps/api apps/api -COPY packages/api-client packages/api-client -COPY packages/supabase packages/supabase -RUN pnpm install --filter @hypr/api --frozen-lockfile -RUN pnpm deploy --filter @hypr/api --prod --legacy /runtime - -FROM oven/bun:1 AS runtime +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ + cargo build --release -p api + +FROM debian:bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* +RUN groupadd -g 1001 appgroup && \ + useradd -u 1001 -g appgroup -d /nonexistent -s /usr/sbin/nologin -M appuser +COPY --from=build --chown=appuser:appgroup /app/target/release/api /usr/local/bin/api +USER appuser WORKDIR /app - -# https://github.com/aptible/supercronic/releases -ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.33/supercronic-linux-amd64 \ - SUPERCRONIC=supercronic-linux-amd64 \ - SUPERCRONIC_SHA1SUM=71b0d58cc53f6bd72cf2f293e09e294b79c666d8 - -RUN apt-get update && apt-get install -y curl && \ - curl -fsSLO "$SUPERCRONIC_URL" && \ - echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - && \ - chmod +x "$SUPERCRONIC" && \ - mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" && \ - ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic && \ - apt-get remove -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* - -COPY --from=build /runtime ./ -COPY apps/api/crontab /app/crontab - -EXPOSE 8787 -CMD ["bun", "src/index.ts"] +EXPOSE 3001 +ENTRYPOINT ["/usr/local/bin/api"] diff --git a/apps/api/Taskfile.yaml b/apps/api/Taskfile.yaml deleted file mode 100644 index 915cf84aa9..0000000000 --- a/apps/api/Taskfile.yaml +++ /dev/null @@ -1,29 +0,0 @@ -version: "3" - -tasks: - deploy: - cmds: - - fly deploy --config apps/api/fly.toml - - env-prod: - dir: . - cmds: - - | - bash <<'EOF' - set -euo pipefail - - env_file=".env.prod" - - if [ ! -f "$env_file" ]; then - exit 1 - fi - - awk ' - /^[[:space:]]*$/ { next } - /^[[:space:]]*#/ { next } - { - sub(/^[[:space:]]+/, "", $0) - print - } - ' "$env_file" | fly secrets import - EOF diff --git a/apps/ai/build.rs b/apps/api/build.rs similarity index 100% rename from apps/ai/build.rs rename to apps/api/build.rs diff --git a/apps/api/crontab b/apps/api/crontab deleted file mode 100644 index e9e1a0f177..0000000000 --- a/apps/api/crontab +++ /dev/null @@ -1 +0,0 @@ -0 * * * * cd /app && echo "TODO" diff --git a/apps/ai/fly.toml b/apps/api/fly.ai.toml similarity index 100% rename from apps/ai/fly.toml rename to apps/api/fly.ai.toml diff --git a/apps/api/fly.toml b/apps/api/fly.toml deleted file mode 100644 index b9d3e30dd2..0000000000 --- a/apps/api/fly.toml +++ /dev/null @@ -1,47 +0,0 @@ -# fly.toml app configuration file generated for hyprnote-api on 2025-11-15T10:28:09+09:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# - -app = 'hyprnote-api' -primary_region = 'sjc' -kill_signal = 'SIGTERM' -kill_timeout = 30 -swap_size_mb = 512 - -[deploy] -strategy = "bluegreen" - -[env] -BUN_ENV = "production" -PORT = "8787" - -[processes] -web = "bun src/index.ts" -cron = "supercronic /app/crontab" - -[http_service] -processes = ['web'] -internal_port = 8787 -force_https = true -auto_stop_machines = 'stop' -auto_start_machines = true -min_machines_running = 1 - -[http_service.concurrency] -type = "connections" -hard_limit = 200 -soft_limit = 150 - -[[http_service.checks]] -grace_period = "20s" -interval = "15s" -method = "GET" -path = "/health" -protocol = "http" -timeout = "4s" - -[[vm]] -memory = '1gb' -cpu_kind = 'shared' -cpus = 1 diff --git a/apps/api/openapi.gen.json b/apps/api/openapi.gen.json index f9c36430ca..c85701e5cd 100644 --- a/apps/api/openapi.gen.json +++ b/apps/api/openapi.gen.json @@ -1,369 +1,119 @@ { "openapi": "3.1.0", "info": { - "title": "Hyprnote API", - "description": "Development documentation", - "version": "1.0.0" - }, - "tags": [ - { - "name": "private" + "title": "Hyprnote AI API", + "description": "AI services API for speech-to-text transcription, LLM chat completions, and subscription management", + "license": { + "name": "" }, - { - "name": "public" - } - ], - "components": { - "securitySchemes": { - "Bearer": { - "type": "http", - "scheme": "bearer" - } - }, - "schemas": {} + "version": "1.0.0" }, - "servers": [ - { - "url": "https://api.hyprnote.com", - "description": "Production server" - }, - { - "url": "http://localhost:4000", - "description": "Local development server" - } - ], "paths": { - "/health": { - "get": { - "responses": { - "200": { - "description": "result", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string" - } - }, - "required": [ - "status" - ], - "additionalProperties": false - } + "/calendar/calendars": { + "post": { + "tags": [ + "calendar" + ], + "operationId": "list_calendars", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListCalendarsRequest" } } - } + }, + "required": true }, - "operationId": "getHealth", - "tags": [ - "private-skip-openapi" - ], - "parameters": [] - } - }, - "/billing/start-trial": { - "post": { "responses": { "200": { - "description": "result", + "description": "Calendars fetched", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "started": { - "type": "boolean" - }, - "reason": { - "type": "string", - "enum": [ - "started", - "not_eligible", - "error" - ] - } - }, - "required": [ - "started" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ListCalendarsResponse" } } } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal server error" } }, - "operationId": "postBillingStart-trial", - "tags": [ - "private" - ], - "parameters": [ + "security": [ { - "in": "query", - "name": "interval", - "schema": { - "default": "monthly", - "type": "string", - "enum": [ - "monthly", - "yearly" - ] - }, - "required": true + "bearer_auth": [] } ] } }, - "/feedback/submit": { + "/calendar/events": { "post": { - "responses": { - "200": { - "description": "Feedback submitted successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "issueUrl": { - "type": "string" - }, - "error": { - "type": "string" - } - }, - "required": [ - "success" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "Invalid request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "issueUrl": { - "type": "string" - }, - "error": { - "type": "string" - } - }, - "required": [ - "success" - ], - "additionalProperties": false - } - } - } - }, - "500": { - "description": "Server error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "issueUrl": { - "type": "string" - }, - "error": { - "type": "string" - } - }, - "required": [ - "success" - ], - "additionalProperties": false - } - } - } - } - }, - "operationId": "postFeedbackSubmit", "tags": [ - "private-skip-openapi" + "calendar" ], - "parameters": [], + "operationId": "list_events", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "bug", - "feature" - ] - }, - "description": { - "type": "string", - "minLength": 10 - }, - "logs": { - "type": "string" - }, - "deviceInfo": { - "type": "object", - "properties": { - "platform": { - "type": "string" - }, - "arch": { - "type": "string" - }, - "osVersion": { - "type": "string" - }, - "appVersion": { - "type": "string" - }, - "gitHash": { - "type": "string" - } - }, - "required": [ - "platform", - "arch", - "osVersion", - "appVersion", - "gitHash" - ], - "additionalProperties": false - } - }, - "required": [ - "type", - "description", - "deviceInfo" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ListEventsRequest" } } - } - } - } - }, - "/file-transcription/start": { - "post": { + }, + "required": true + }, "responses": { "200": { - "description": "Pipeline started", + "description": "Events fetched", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "pipelineId": { - "type": "string" - }, - "invocationId": { - "type": "string" - } - }, - "required": [ - "pipelineId", - "invocationId" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ListEventsResponse" } } } }, - "400": { - "description": "Invalid fileId" - }, "401": { "description": "Unauthorized" }, "500": { - "description": "Internal error" + "description": "Internal server error" } }, - "operationId": "postFile-transcriptionStart", - "tags": [ - "private" - ], - "parameters": [], "security": [ { - "Bearer": [] + "bearer_auth": [] } + ] + } + }, + "/calendar/events/create": { + "post": { + "tags": [ + "calendar" ], + "operationId": "create_event", "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "fileId": { - "type": "string" - }, - "pipelineId": { - "type": "string" - } - }, - "required": [ - "fileId" - ], - "additionalProperties": false + "$ref": "#/components/schemas/CreateEventRequest" } } - } - } - } - }, - "/file-transcription/status/{pipelineId}": { - "get": { + }, + "required": true + }, "responses": { "200": { - "description": "Pipeline status", + "description": "Event created", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "QUEUED", - "TRANSCRIBING", - "TRANSCRIBED", - "LLM_RUNNING", - "DONE", - "ERROR" - ] - }, - "transcript": { - "type": "string" - }, - "llmResult": { - "type": "string" - }, - "error": { - "type": "string" - } - }, - "required": [ - "status" - ], - "additionalProperties": false + "$ref": "#/components/schemas/CreateEventResponse" } } } @@ -371,69 +121,30 @@ "401": { "description": "Unauthorized" }, - "403": { - "description": "Forbidden" - }, "500": { - "description": "Internal error" + "description": "Internal server error" } }, - "operationId": "getFile-transcriptionStatusByPipelineId", - "tags": [ - "private" - ], - "parameters": [ - { - "in": "path", - "name": "pipelineId", - "schema": { - "type": "string" - }, - "required": true - } - ], "security": [ { - "Bearer": [] + "bearer_auth": [] } ] } }, - "/file-transcription/result/{pipelineId}": { - "get": { + "/nango/connect-session": { + "post": { + "tags": [ + "nango" + ], + "operationId": "create_connect_session", "responses": { "200": { - "description": "Pipeline result", + "description": "Connect session created", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "QUEUED", - "TRANSCRIBING", - "TRANSCRIBED", - "LLM_RUNNING", - "DONE", - "ERROR" - ] - }, - "transcript": { - "type": "string" - }, - "llmResult": { - "type": "string" - }, - "error": { - "type": "string" - } - }, - "required": [ - "status" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ConnectSessionResponse" } } } @@ -441,153 +152,549 @@ "401": { "description": "Unauthorized" }, - "403": { - "description": "Forbidden" - }, "500": { - "description": "Internal error" + "description": "Internal server error" } }, - "operationId": "getFile-transcriptionResultByPipelineId", - "tags": [ - "private" - ], - "parameters": [ - { - "in": "path", - "name": "pipelineId", - "schema": { - "type": "string" - }, - "required": true - } - ], "security": [ { - "Bearer": [] + "bearer_auth": [] } ] } }, - "/rpc/can-start-trial": { - "get": { + "/nango/webhook": { + "post": { + "tags": [ + "nango" + ], + "operationId": "nango_webhook", "responses": { "200": { - "description": "result", + "description": "Webhook processed", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "canStartTrial": { - "type": "boolean" - }, - "reason": { - "type": "string", - "enum": [ - "eligible", - "has_active_subscription", - "had_recent_trial", - "error" - ] - } - }, - "required": [ - "canStartTrial" - ], - "additionalProperties": false + "$ref": "#/components/schemas/WebhookResponse" } } } + }, + "400": { + "description": "Bad request" + }, + "401": { + "description": "Invalid signature" } - }, - "operationId": "getRpcCan-start-trial", - "tags": [ - "private" - ], - "parameters": [] + } } }, - "/webhook/stripe": { - "post": { + "/subscription/can-start-trial": { + "get": { + "tags": [ + "subscription" + ], + "operationId": "can_start_trial", "responses": { "200": { - "description": "result", + "description": "Check successful", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "ok": { - "type": "boolean" - } - }, - "required": [ - "ok" - ], - "additionalProperties": false + "$ref": "#/components/schemas/CanStartTrialResponse" } } } }, - "400": { - "description": "-" + "401": { + "description": "Unauthorized" }, "500": { - "description": "-" + "description": "Internal server error" } }, - "operationId": "postWebhookStripe", + "security": [ + { + "bearer_auth": [] + } + ] + } + }, + "/subscription/start-trial": { + "post": { "tags": [ - "private-skip-openapi" + "subscription" ], + "operationId": "start_trial", "parameters": [ { - "in": "header", - "name": "stripe-signature", + "name": "interval", + "in": "query", + "required": false, "schema": { - "type": "string" + "$ref": "#/components/schemas/Interval" }, - "required": true + "example": "monthly" + } + ], + "responses": { + "200": { + "description": "Trial started successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartTrialResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "bearer_auth": [] } ] } }, - "/webhook/slack/events": { + "/feedback/submit": { "post": { + "tags": [ + "feedback" + ], + "operationId": "submit", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackRequest" + } + } + }, + "required": true + }, "responses": { "200": { - "description": "result", + "description": "Feedback submitted successfully", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "ok": { - "type": "boolean" - } - }, - "required": [ - "ok" - ], - "additionalProperties": false + "$ref": "#/components/schemas/FeedbackResponse" } } } }, "400": { - "description": "-" + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackResponse" + } + } + } }, "500": { - "description": "-" + "description": "Server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackResponse" + } + } + } } - }, - "operationId": "postWebhookSlackEvents", - "tags": [ - "private-skip-openapi" + } + } + } + }, + "components": { + "schemas": { + "CanStartTrialResponse": { + "type": "object", + "required": [ + "canStartTrial" + ], + "properties": { + "canStartTrial": { + "type": "boolean", + "example": true + } + } + }, + "ConnectSessionResponse": { + "type": "object", + "required": [ + "token", + "expires_at" + ], + "properties": { + "expires_at": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "CreateEventRequest": { + "type": "object", + "required": [ + "connection_id", + "calendar_id", + "summary", + "start", + "end" + ], + "properties": { + "attendees": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/EventAttendee" + } + }, + "calendar_id": { + "type": "string" + }, + "connection_id": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "end": { + "$ref": "#/components/schemas/EventDateTime" + }, + "location": { + "type": [ + "string", + "null" + ] + }, + "start": { + "$ref": "#/components/schemas/EventDateTime" + }, + "summary": { + "type": "string" + } + } + }, + "CreateEventResponse": { + "type": "object", + "required": [ + "event" + ], + "properties": { + "event": {} + } + }, + "DeviceInfo": { + "type": "object", + "required": [ + "platform", + "arch", + "osVersion", + "appVersion" ], - "parameters": [] + "properties": { + "appVersion": { + "type": "string" + }, + "arch": { + "type": "string" + }, + "osVersion": { + "type": "string" + }, + "platform": { + "type": "string" + } + } + }, + "EventAttendee": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "displayName": { + "type": [ + "string", + "null" + ] + }, + "email": { + "type": "string" + }, + "optional": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "EventDateTime": { + "type": "object", + "properties": { + "date": { + "type": [ + "string", + "null" + ] + }, + "dateTime": { + "type": [ + "string", + "null" + ] + }, + "timeZone": { + "type": [ + "string", + "null" + ] + } + } + }, + "FeedbackRequest": { + "type": "object", + "required": [ + "type", + "description", + "deviceInfo" + ], + "properties": { + "description": { + "type": "string" + }, + "deviceInfo": { + "$ref": "#/components/schemas/DeviceInfo" + }, + "logs": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/components/schemas/FeedbackType" + } + } + }, + "FeedbackResponse": { + "type": "object", + "required": [ + "success" + ], + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "issueUrl": { + "type": [ + "string", + "null" + ] + }, + "success": { + "type": "boolean" + } + } + }, + "FeedbackType": { + "type": "string", + "enum": [ + "bug", + "feature" + ] + }, + "Interval": { + "type": "string", + "enum": [ + "monthly", + "yearly" + ] + }, + "ListCalendarsRequest": { + "type": "object", + "required": [ + "connection_id" + ], + "properties": { + "connection_id": { + "type": "string" + } + } + }, + "ListCalendarsResponse": { + "type": "object", + "required": [ + "calendars" + ], + "properties": { + "calendars": { + "type": "array", + "items": {} + } + } + }, + "ListEventsRequest": { + "type": "object", + "required": [ + "connection_id", + "calendar_id" + ], + "properties": { + "calendar_id": { + "type": "string" + }, + "connection_id": { + "type": "string" + }, + "max_results": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "minimum": 0 + }, + "order_by": { + "type": [ + "string", + "null" + ] + }, + "page_token": { + "type": [ + "string", + "null" + ] + }, + "single_events": { + "type": [ + "boolean", + "null" + ] + }, + "time_max": { + "type": [ + "string", + "null" + ] + }, + "time_min": { + "type": [ + "string", + "null" + ] + } + } + }, + "ListEventsResponse": { + "type": "object", + "required": [ + "events" + ], + "properties": { + "events": { + "type": "array", + "items": {} + }, + "next_page_token": { + "type": [ + "string", + "null" + ] + } + } + }, + "StartTrialResponse": { + "type": "object", + "required": [ + "started" + ], + "properties": { + "started": { + "type": "boolean", + "example": true + } + } + }, + "WebhookResponse": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "bearer_auth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Supabase JWT token" + }, + "device_fingerprint": { + "type": "apiKey", + "in": "header", + "name": "x-device-fingerprint", + "description": "Optional device fingerprint for analytics" } } - } + }, + "tags": [ + { + "name": "stt", + "description": "Speech-to-text transcription endpoints" + }, + { + "name": "llm", + "description": "LLM chat completions endpoints" + }, + { + "name": "calendar", + "description": "Calendar management" + }, + { + "name": "nango", + "description": "Integration management via Nango" + }, + { + "name": "subscription", + "description": "Subscription and trial management" + }, + { + "name": "transcribe", + "description": "Speech-to-text transcription proxy" + }, + { + "name": "llm", + "description": "LLM chat completions proxy" + }, + { + "name": "support", + "description": "User feedback and support" + } + ] } \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json deleted file mode 100644 index 7777ed050c..0000000000 --- a/apps/api/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@hypr/api", - "private": true, - "type": "module", - "scripts": { - "dev": "dotenvx run --ignore MISSING_ENV_FILE -f ../../.env.supabase -f ../../.env.stripe -f ../../.env.restate -f .env -- bun --hot src/index.ts", - "stripe:migrate": "dotenvx run --ignore MISSING_ENV_FILE -f ../../.env.supabase -f ../../.env.stripe -f .env -- bun src/scripts/run-stripe-migrations.ts", - "openapi": "CI=true bun src/scripts/generate-openapi.ts", - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@hono/zod-validator": "^0.7.6", - "@hypr/api-client": "workspace:*", - "@hypr/supabase": "workspace:*", - "@octokit/auth-app": "^8.1.2", - "@octokit/rest": "^22.0.1", - "@posthog/ai": "^7.8.2", - "@restatedev/restate-sdk-clients": "^1.10.2", - "@scalar/hono-api-reference": "^0.5.184", - "@sentry/bun": "^10.38.0", - "@supabase/supabase-js": "^2.93.3", - "@t3-oss/env-core": "^0.13.10", - "@types/pg": "^8.16.0", - "effect": "^3.19.15", - "hono": "^4.11.7", - "hono-openapi": "^0.4.8", - "openai": "^6.17.0", - "pg": "^8.18.0", - "posthog-node": "^5.24.7", - "stripe": "^19.3.1", - "zod": "^4.3.6", - "zod-openapi": "^5.4.6" - }, - "devDependencies": { - "@dotenvx/dotenvx": "^1.52.0", - "@hey-api/openapi-ts": "^0.91.1", - "@types/bun": "^1.3.8", - "typescript": "^5.9.3" - } -} diff --git a/apps/ai/src/auth.rs b/apps/api/src/auth.rs similarity index 100% rename from apps/ai/src/auth.rs rename to apps/api/src/auth.rs diff --git a/apps/api/src/billing.ts b/apps/api/src/billing.ts deleted file mode 100644 index c7fc541494..0000000000 --- a/apps/api/src/billing.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Stripe from "stripe"; - -import { stripe } from "./integration/stripe"; -import { supabaseAdmin } from "./integration/supabase"; - -const CUSTOMER_EVENTS: Stripe.Event.Type[] = [ - "checkout.session.completed", - "customer.created", - "customer.updated", - "customer.subscription.created", - "customer.subscription.updated", -]; - -export async function syncBillingBridge(event: Stripe.Event) { - if (!isCustomerEvent(event.type)) { - return; - } - - const customerId = getCustomerId(event.data.object); - - if (!customerId) { - return; - } - - const customer = await getStripeCustomer(customerId); - - if (!customer) { - return; - } - - const userId = getUserIdFromCustomer(customer); - - if (!userId) { - return; - } - - const { error } = await supabaseAdmin.from("profiles").upsert( - { - id: userId, - stripe_customer_id: customerId, - }, - { onConflict: "id" }, - ); - - if (error) { - throw error; - } -} - -const isCustomerEvent = (eventType: string) => - CUSTOMER_EVENTS.includes(eventType as Stripe.Event.Type); - -const getCustomerId = ( - eventObject: Stripe.Event.Data.Object, -): string | null => { - const obj = eventObject as { - customer?: string | { id: string }; - id?: string; - }; - - if (typeof obj.customer === "string") { - return obj.customer; - } - - if (obj.customer && typeof obj.customer === "object") { - return obj.customer.id; - } - - if (obj.id?.startsWith("cus_")) { - return obj.id; - } - - return null; -}; - -const getStripeCustomer = async (customerId: string) => { - const customer = await stripe.customers.retrieve(customerId); - - if (isDeletedCustomer(customer)) { - return null; - } - - return customer; -}; - -const isDeletedCustomer = ( - customer: Stripe.Customer | Stripe.DeletedCustomer, -): customer is Stripe.DeletedCustomer => - "deleted" in customer && customer.deleted === true; - -const getUserIdFromCustomer = (customer: Stripe.Customer): string | null => { - const metadata = customer.metadata ?? {}; - - return ( - metadata["userId"] || metadata["user_id"] || metadata["userID"] || null - ); -}; diff --git a/apps/ai/src/env.rs b/apps/api/src/env.rs similarity index 100% rename from apps/ai/src/env.rs rename to apps/api/src/env.rs diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts deleted file mode 100644 index fd1003a77b..0000000000 --- a/apps/api/src/env.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createEnv } from "@t3-oss/env-core"; -import { z } from "zod"; - -export const env = createEnv({ - server: { - PORT: z.coerce.number().default(8787), - APP_VERSION: - process.env.NODE_ENV === "production" - ? z.string().min(1) // Set in `api_cd.yaml` via the Fly CLI - : z.string().optional(), - NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), - LOAD_TEST: z.coerce.boolean().default(false), - DATABASE_URL: z.string().min(1), - SUPABASE_URL: z.url(), - SUPABASE_ANON_KEY: z.string().min(1), - SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), - STRIPE_SECRET_KEY: z.string().min(1), - STRIPE_MONTHLY_PRICE_ID: z.string().min(1), - STRIPE_YEARLY_PRICE_ID: z.string().min(1), - OPENROUTER_API_KEY: z.string().min(1), - DEEPGRAM_API_KEY: z.string().min(1), - ASSEMBLYAI_API_KEY: z.string().min(1), - SONIOX_API_KEY: z.string().min(1), - POSTHOG_API_KEY: z.string().min(1), - RESTATE_INGRESS_URL: z.url(), - OVERRIDE_AUTH: z.string().optional(), - SLACK_BOT_TOKEN: z.string().optional(), - SLACK_SIGNING_SECRET: z.string().optional(), - LOOPS_API_KEY: z.string().optional(), - LOOPS_SLACK_CHANNEL_ID: z.string().optional(), - CHARLIE_APP_ID: z.string().optional(), - CHARLIE_APP_PRIVATE_KEY: z.string().optional(), - CHARLIE_APP_INSTALLATION_ID: z.string().optional(), - }, - runtimeEnv: Bun.env, - emptyStringAsUndefined: true, - skipValidation: Bun.env.CI === "true", -}); diff --git a/apps/api/src/hono-bindings.ts b/apps/api/src/hono-bindings.ts deleted file mode 100644 index 72948bf49f..0000000000 --- a/apps/api/src/hono-bindings.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type * as Sentry from "@sentry/bun"; -import type Stripe from "stripe"; - -import type { SupabaseAuthBindings } from "@hypr/supabase/middleware"; - -import type { Emitter } from "./observability"; - -export type AppBindings = SupabaseAuthBindings & { - Variables: SupabaseAuthBindings["Variables"] & { - stripeEvent: Stripe.Event; - stripeRawBody: string; - stripeSignature: string; - slackRawBody: string; - slackTimestamp: string; - sentrySpan: Sentry.Span; - emit: Emitter; - }; -}; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts deleted file mode 100644 index 37a34c604e..0000000000 --- a/apps/api/src/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -import "./instrument"; - -import { apiReference } from "@scalar/hono-api-reference"; -import * as Sentry from "@sentry/bun"; -import { Hono } from "hono"; -import { openAPISpecs } from "hono-openapi"; -import { bodyLimit } from "hono/body-limit"; -import { websocket } from "hono/bun"; -import { cors } from "hono/cors"; -import { logger } from "hono/logger"; - -import { env } from "./env"; -import type { AppBindings } from "./hono-bindings"; -import { - loadTestOverride, - observabilityMiddleware, - sentryMiddleware, - supabaseAuthMiddleware, - verifySlackWebhook, -} from "./middleware"; -import { openAPIDocumentation } from "./openapi"; -import { routes } from "./routes"; - -const app = new Hono(); - -app.use(sentryMiddleware); -app.use(observabilityMiddleware); -app.use(logger()); -app.use(bodyLimit({ maxSize: 1024 * 1024 * 5 })); - -const corsMiddleware = cors({ - origin: "*", - allowHeaders: [ - "authorization", - "x-client-info", - "x-device-fingerprint", - "apikey", - "content-type", - "user-agent", - "sentry-trace", - "baggage", - ], - allowMethods: ["GET", "POST", "OPTIONS"], -}); - -app.use("*", (c, next) => { - if (c.req.path === "/listen") { - return next(); - } - return corsMiddleware(c, next); -}); - -app.use("/webhook/slack/events", verifySlackWebhook); - -app.route("/", routes); - -app.onError((err, c) => { - Sentry.captureException(err, { - extra: { path: c.req.path, method: c.req.method }, - }); - return c.json({ error: "internal_server_error" }, 500); -}); - -app.notFound((c) => c.text("not_found", 404)); - -app.get( - "/openapi.gen.json", - openAPISpecs(routes, { documentation: openAPIDocumentation }), -); - -app.get( - "/docs", - apiReference({ - theme: "saturn", - spec: { - url: "/openapi.gen.json", - }, - }), -); - -export default { - port: env.PORT, - fetch: app.fetch, - websocket, -}; diff --git a/apps/api/src/instrument.js b/apps/api/src/instrument.js deleted file mode 100644 index dff33f0c00..0000000000 --- a/apps/api/src/instrument.js +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sentry from "@sentry/bun"; - -const isProduction = Bun.env.BUN_ENV === "production"; - -Sentry.init({ - dsn: Bun.env.SENTRY_DSN, - environment: isProduction ? "production" : "development", - release: Bun.env.APP_VERSION - ? `hyprnote-api@${Bun.env.APP_VERSION}` - : "hyprnote-api@local", - sampleRate: 1.0, - enabled: isProduction || ["true", "1"].includes(Bun.env.LOAD_TEST), - initialScope: { - tags: { - service: "api", - }, - }, -}); diff --git a/apps/api/src/integration/index.ts b/apps/api/src/integration/index.ts deleted file mode 100644 index 97021e9093..0000000000 --- a/apps/api/src/integration/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./supabase"; -export * from "./stripe"; -export * from "./posthog"; -export * from "./restate"; diff --git a/apps/api/src/integration/loops.ts b/apps/api/src/integration/loops.ts deleted file mode 100644 index 0b96f81e10..0000000000 --- a/apps/api/src/integration/loops.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { env } from "../env"; - -export interface LoopsContact { - id: string; - email: string; - source?: string; - intent?: string; - platform?: string; - firstName?: string; - lastName?: string; - userGroup?: string; - subscribed: boolean; - createdAt: string; - updatedAt: string; -} - -export type ContactStatus = "paid" | "signed up" | "interested" | "unknown"; - -export function classifyContactStatus(contact: LoopsContact): ContactStatus { - const { source, intent, platform } = contact; - - if (source === "Stripe webhook") { - return "paid"; - } - - if (source === "Supabase webhook") { - return "signed up"; - } - - if ( - source === "LANDING_PAGE" && - intent === "Waitlist" && - (platform === "Windows" || platform === "Linux") - ) { - return "interested"; - } - - return "unknown"; -} - -export async function getContactByEmail( - email: string, -): Promise { - if (!env.LOOPS_API_KEY) { - throw new Error("LOOPS_API_KEY not configured"); - } - - const response = await fetch( - `https://app.loops.so/api/v1/contacts/find?email=${encodeURIComponent(email)}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${env.LOOPS_API_KEY}`, - }, - }, - ); - - if (!response.ok) { - if (response.status === 404) { - return null; - } - throw new Error(`Failed to fetch contact: ${response.statusText}`); - } - - const contacts = await response.json(); - if (Array.isArray(contacts) && contacts.length > 0) { - return contacts[0] as LoopsContact; - } - return null; -} diff --git a/apps/api/src/integration/posthog.ts b/apps/api/src/integration/posthog.ts deleted file mode 100644 index 30a63e03d9..0000000000 --- a/apps/api/src/integration/posthog.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PostHog } from "posthog-node"; - -import { env } from "../env"; - -export const posthog = new PostHog(env.POSTHOG_API_KEY, { - host: "https://us.i.posthog.com", - disabled: env.LOAD_TEST, -}); diff --git a/apps/api/src/integration/restate.ts b/apps/api/src/integration/restate.ts deleted file mode 100644 index d2b276debc..0000000000 --- a/apps/api/src/integration/restate.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as clients from "@restatedev/restate-sdk-clients"; - -import { env } from "../env"; - -let restateClientInstance: ReturnType | null = null; - -export function getRestateClient() { - if (!restateClientInstance) { - restateClientInstance = clients.connect({ - url: env.RESTATE_INGRESS_URL, - }); - } - return restateClientInstance; -} diff --git a/apps/api/src/integration/slack.ts b/apps/api/src/integration/slack.ts deleted file mode 100644 index b5cebda28d..0000000000 --- a/apps/api/src/integration/slack.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { env } from "../env"; - -interface SlackPostMessageResponse { - ok: boolean; - channel?: string; - ts?: string; - error?: string; -} - -const SLACK_TIMEOUT_MS = 5000; - -export async function postThreadReply( - channel: string, - threadTs: string, - text: string, -): Promise { - if (!env.SLACK_BOT_TOKEN) { - throw new Error("SLACK_BOT_TOKEN not configured"); - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), SLACK_TIMEOUT_MS); - - try { - const response = await fetch("https://slack.com/api/chat.postMessage", { - method: "POST", - headers: { - Authorization: `Bearer ${env.SLACK_BOT_TOKEN}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - channel, - thread_ts: threadTs, - text, - }), - signal: controller.signal, - }); - - if (!response.ok) { - throw new Error( - `Failed to post Slack message: ${response.status} ${response.statusText}`, - ); - } - - const result: SlackPostMessageResponse = await response.json(); - - if (!result.ok) { - throw new Error(`Slack API error: ${result.error || "unknown error"}`); - } - - return result; - } catch (error) { - if (error instanceof Error && error.name === "AbortError") { - throw new Error( - `Slack API request timed out after ${SLACK_TIMEOUT_MS}ms`, - ); - } - throw error; - } finally { - clearTimeout(timeoutId); - } -} diff --git a/apps/api/src/integration/stripe.ts b/apps/api/src/integration/stripe.ts deleted file mode 100644 index a0e142da73..0000000000 --- a/apps/api/src/integration/stripe.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Stripe from "stripe"; - -import { env } from "../env"; - -export const STRIPE_API_VERSION = "2025-10-29.clover"; - -export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, -}); diff --git a/apps/api/src/integration/supabase.ts b/apps/api/src/integration/supabase.ts deleted file mode 100644 index 7370a99538..0000000000 --- a/apps/api/src/integration/supabase.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createClient } from "@hypr/supabase"; - -import { env } from "../env"; - -export const supabaseAdmin = createClient( - env.SUPABASE_URL, - env.SUPABASE_SERVICE_ROLE_KEY, -); diff --git a/apps/ai/src/main.rs b/apps/api/src/main.rs similarity index 100% rename from apps/ai/src/main.rs rename to apps/api/src/main.rs diff --git a/apps/api/src/middleware/index.ts b/apps/api/src/middleware/index.ts deleted file mode 100644 index fd62c1ad08..0000000000 --- a/apps/api/src/middleware/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./load-test-auth"; -export * from "./observability"; -export * from "./sentry"; -export * from "./slack"; -export * from "./supabase"; diff --git a/apps/api/src/middleware/load-test-auth.ts b/apps/api/src/middleware/load-test-auth.ts deleted file mode 100644 index 003e732ef9..0000000000 --- a/apps/api/src/middleware/load-test-auth.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createMiddleware } from "hono/factory"; - -import { env } from "../env"; - -export const loadTestOverride = createMiddleware<{ - Variables: { supabaseUserId: string }; -}>(async (c, next) => { - if (env.OVERRIDE_AUTH) { - const token = c.req.header("Authorization")?.replace(/^bearer /i, ""); - if (token === env.OVERRIDE_AUTH) { - c.set("supabaseUserId", "load-test-user"); - return next(); - } - } - return next(); -}); diff --git a/apps/api/src/middleware/observability.ts b/apps/api/src/middleware/observability.ts deleted file mode 100644 index 5665f547a0..0000000000 --- a/apps/api/src/middleware/observability.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { createMiddleware } from "hono/factory"; - -import type { AppBindings } from "../hono-bindings"; -import { - type Emitter, - handlePosthog, - handleSentry, - type ObservabilityEvent, -} from "../observability"; - -const emit: Emitter = (event: ObservabilityEvent) => { - try { - handlePosthog(event); - } catch (e) { - Sentry.captureException(e, { extra: { event, handler: "posthog" } }); - } - - try { - handleSentry(event); - } catch (e) { - Sentry.captureException(e, { extra: { event, handler: "sentry" } }); - } -}; - -export const observabilityMiddleware = createMiddleware( - async (c, next) => { - c.set("emit", emit); - await next(); - }, -); diff --git a/apps/api/src/middleware/sentry.ts b/apps/api/src/middleware/sentry.ts deleted file mode 100644 index 51ae1c3471..0000000000 --- a/apps/api/src/middleware/sentry.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { createMiddleware } from "hono/factory"; - -import type { AppBindings } from "../hono-bindings"; - -export const sentryMiddleware = createMiddleware( - async (c, next) => { - const sentryTrace = c.req.header("sentry-trace"); - const baggage = c.req.header("baggage"); - - return Sentry.continueTrace({ sentryTrace, baggage }, async () => { - return Sentry.startSpan( - { - name: `${c.req.method} ${c.req.path}`, - op: "http.server", - attributes: { - "http.method": c.req.method, - "http.url": c.req.url, - }, - }, - async (span) => { - c.set("sentrySpan", span); - await next(); - span.setAttribute("http.status_code", c.res.status); - }, - ); - }); - }, -); diff --git a/apps/api/src/middleware/slack.ts b/apps/api/src/middleware/slack.ts deleted file mode 100644 index 15ac18c79f..0000000000 --- a/apps/api/src/middleware/slack.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { createMiddleware } from "hono/factory"; - -import { env } from "../env"; - -export const verifySlackWebhook = createMiddleware<{ - Variables: { - slackRawBody: string; - slackTimestamp: string; - }; -}>(async (c, next) => { - console.log("[slack middleware] Starting verification"); - - if (!env.SLACK_SIGNING_SECRET) { - console.log("[slack middleware] SLACK_SIGNING_SECRET not configured"); - return c.text("slack_signing_secret_not_configured", 500); - } - - const signature = c.req.header("X-Slack-Signature"); - const timestamp = c.req.header("X-Slack-Request-Timestamp"); - - console.log("[slack middleware] signature:", signature); - console.log("[slack middleware] timestamp:", timestamp); - - if (!signature || !timestamp) { - console.log("[slack middleware] Missing signature or timestamp"); - return c.text("missing_slack_signature", 400); - } - - const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5; - if (Number.parseInt(timestamp) < fiveMinutesAgo) { - console.log("[slack middleware] Request too old, timestamp:", timestamp); - return c.text("slack_request_too_old", 400); - } - - const body = await c.req.text(); - console.log("[slack middleware] Request body:", body); - - try { - const sigBaseString = `v0:${timestamp}:${body}`; - const encoder = new TextEncoder(); - const key = await crypto.subtle.importKey( - "raw", - encoder.encode(env.SLACK_SIGNING_SECRET), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], - ); - const signatureBuffer = await crypto.subtle.sign( - "HMAC", - key, - encoder.encode(sigBaseString), - ); - const computedSignature = `v0=${Array.from(new Uint8Array(signatureBuffer)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("")}`; - - console.log("[slack middleware] computedSignature:", computedSignature); - console.log("[slack middleware] receivedSignature:", signature); - - if (computedSignature !== signature) { - console.log("[slack middleware] Signature mismatch!"); - return c.text("invalid_slack_signature", 400); - } - - console.log("[slack middleware] Signature verified, proceeding"); - c.set("slackRawBody", body); - c.set("slackTimestamp", timestamp); - await next(); - } catch (err) { - console.log("[slack middleware] Error during verification:", err); - Sentry.captureException(err, { - tags: { webhook: "slack", step: "signature_verification" }, - }); - const message = err instanceof Error ? err.message : "unknown_error"; - return c.text(message, 400); - } -}); diff --git a/apps/api/src/middleware/supabase.ts b/apps/api/src/middleware/supabase.ts deleted file mode 100644 index 4c4d9c014b..0000000000 --- a/apps/api/src/middleware/supabase.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createSupabaseAuthMiddleware } from "@hypr/supabase/middleware"; - -import { env } from "../env"; -import type { AppBindings } from "../hono-bindings"; - -export const supabaseAuthMiddleware = createSupabaseAuthMiddleware( - { - supabaseUrl: env.SUPABASE_URL, - supabaseAnonKey: env.SUPABASE_ANON_KEY, - }, -); diff --git a/apps/api/src/observability/handlers.ts b/apps/api/src/observability/handlers.ts deleted file mode 100644 index a4a22c991f..0000000000 --- a/apps/api/src/observability/handlers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ObservabilityEvent } from "./types"; - -export function handlePosthog(_event: ObservabilityEvent): void {} - -export function handleSentry(_event: ObservabilityEvent): void {} diff --git a/apps/api/src/observability/index.ts b/apps/api/src/observability/index.ts deleted file mode 100644 index de457d1ade..0000000000 --- a/apps/api/src/observability/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { Emitter, ObservabilityEvent } from "./types"; -export { handlePosthog, handleSentry } from "./handlers"; diff --git a/apps/api/src/observability/types.ts b/apps/api/src/observability/types.ts deleted file mode 100644 index 0ee55c96b6..0000000000 --- a/apps/api/src/observability/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type ObservabilityEvent = never; - -export type Emitter = (event: ObservabilityEvent) => void; diff --git a/apps/ai/src/openapi.rs b/apps/api/src/openapi.rs similarity index 100% rename from apps/ai/src/openapi.rs rename to apps/api/src/openapi.rs diff --git a/apps/api/src/openapi.ts b/apps/api/src/openapi.ts deleted file mode 100644 index b7f8f78146..0000000000 --- a/apps/api/src/openapi.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { API_TAGS } from "./routes"; - -export const openAPIDocumentation = { - openapi: "3.1.0", - info: { - title: "Hyprnote API", - version: "1.0.0", - }, - tags: [{ name: API_TAGS.PRIVATE }, { name: API_TAGS.PUBLIC }], - components: { - securitySchemes: { - Bearer: { - type: "http" as const, - scheme: "bearer", - }, - }, - }, - servers: [ - { - url: "https://api.hyprnote.com", - description: "Production server", - }, - { - url: "http://localhost:4000", - description: "Local development server", - }, - ], -}; diff --git a/apps/api/src/routes/billing.ts b/apps/api/src/routes/billing.ts deleted file mode 100644 index 040d5d593a..0000000000 --- a/apps/api/src/routes/billing.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { resolver, validator } from "hono-openapi/zod"; -import { z } from "zod"; - -import { env } from "../env"; -import type { AppBindings } from "../hono-bindings"; -import { stripe } from "../integration/stripe"; -import { supabaseAuthMiddleware } from "../middleware/supabase"; -import { API_TAGS } from "./constants"; - -const StartTrialQuerySchema = z.object({ - interval: z.enum(["monthly", "yearly"]).default("monthly"), -}); - -const StartTrialResponseSchema = z.object({ - started: z.boolean(), - reason: z.enum(["started", "not_eligible", "error"]).optional(), -}); - -export const billing = new Hono(); - -billing.post( - "/start-trial", - describeRoute({ - tags: [API_TAGS.PRIVATE], - responses: { - 200: { - description: "result", - content: { - "application/json": { - schema: resolver(StartTrialResponseSchema), - }, - }, - }, - }, - }), - validator("query", StartTrialQuerySchema), - supabaseAuthMiddleware, - async (c) => { - const { interval } = c.req.valid("query"); - const supabase = c.get("supabaseClient"); - if (!supabase) { - return c.json({ error: "Supabase client missing" }, 500); - } - const userId = c.get("supabaseUserId"); - if (!userId) { - return c.json({ error: "User ID missing" }, 500); - } - - const { data: canTrial, error: trialError } = - await supabase.rpc("can_start_trial"); - - if (trialError || !canTrial) { - if (trialError) { - console.error("can_start_trial RPC failed in start-trial:", trialError); - } - const reason = trialError ? "error" : "not_eligible"; - return c.json({ started: false, reason }); - } - - const { data: profile } = await supabase - .from("profiles") - .select("stripe_customer_id") - .eq("id", userId) - .single(); - - let stripeCustomerId = profile?.stripe_customer_id as - | string - | null - | undefined; - - if (!stripeCustomerId) { - const { data: user } = await supabase.auth.getUser(); - - const newCustomer = await stripe.customers.create( - { email: user.user?.email, metadata: { userId } }, - { idempotencyKey: `create-customer-${userId}` }, - ); - - await supabase - .from("profiles") - .update({ stripe_customer_id: newCustomer.id }) - .eq("id", userId) - .is("stripe_customer_id", null); - - const { data: updated } = await supabase - .from("profiles") - .select("stripe_customer_id") - .eq("id", userId) - .single(); - - stripeCustomerId = updated?.stripe_customer_id; - } - - if (!stripeCustomerId) { - return c.json({ error: "stripe_customer_id_missing" }, 500); - } - - const priceId = - interval === "yearly" - ? env.STRIPE_YEARLY_PRICE_ID - : env.STRIPE_MONTHLY_PRICE_ID; - - try { - await stripe.subscriptions.create( - { - customer: stripeCustomerId, - items: [{ price: priceId }], - trial_period_days: 14, - trial_settings: { - end_behavior: { missing_payment_method: "cancel" }, - }, - }, - { - idempotencyKey: `trial-${userId}-${new Date().toISOString().slice(0, 10)}`, - }, - ); - } catch (error) { - const errorMessage = - error instanceof Error - ? `Failed to create Stripe subscription: ${error.message}` - : "Failed to create Stripe subscription: unknown error"; - const errorDetails = error instanceof Error ? error.stack : String(error); - - if (env.NODE_ENV !== "production") { - console.error(errorMessage, errorDetails); - } else { - Sentry.captureException(error, { - tags: { - billing: "start_trial", - operation: "create_subscription", - }, - extra: { userId, stripeCustomerId, priceId, errorDetails }, - }); - } - return c.json({ error: "failed_to_create_subscription" }, 500); - } - - return c.json({ started: true, reason: "started" }); - }, -); diff --git a/apps/api/src/routes/constants.ts b/apps/api/src/routes/constants.ts deleted file mode 100644 index 0a4ea2b316..0000000000 --- a/apps/api/src/routes/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const API_TAGS = { - PRIVATE_SKIP_OPENAPI: "private-skip-openapi", - PRIVATE: "private", - PUBLIC: "public", -} as const; diff --git a/apps/api/src/routes/feedback.ts b/apps/api/src/routes/feedback.ts deleted file mode 100644 index b5127dfe1e..0000000000 --- a/apps/api/src/routes/feedback.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { createAppAuth } from "@octokit/auth-app"; -import { Octokit } from "@octokit/rest"; -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { resolver, validator } from "hono-openapi/zod"; -import { z } from "zod"; - -import { env } from "../env"; -import type { AppBindings } from "../hono-bindings"; -import { API_TAGS } from "./constants"; - -const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1/chat/completions"; - -const FeedbackRequestSchema = z.object({ - description: z.string().min(10), - logs: z.string().optional(), - deviceInfo: z.object({ - platform: z.string(), - arch: z.string(), - osVersion: z.string(), - appVersion: z.string(), - }), -}); - -const FeedbackResponseSchema = z.object({ - success: z.boolean(), - issueUrl: z.string().optional(), - error: z.string().optional(), -}); - -async function analyzeLogsWithAI(logs: string): Promise { - try { - const response = await fetch(OPENROUTER_BASE_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${env.OPENROUTER_API_KEY}`, - }, - body: JSON.stringify({ - model: "google/gemini-2.0-flash-001", - max_tokens: 300, - messages: [ - { - role: "user", - content: `Extract only ERROR and WARNING entries from these logs. Output max 800 chars, no explanation:\n\n${logs.slice(-10000)}`, - }, - ], - }), - }); - - if (!response.ok) { - return null; - } - - const data = (await response.json()) as { - choices?: Array<{ message?: { content?: string } }>; - }; - const content = data.choices?.[0]?.message?.content; - return content ? content.slice(0, 800) : null; - } catch { - return null; - } -} - -function getGitHubClient(): Octokit | null { - if ( - !env.CHARLIE_APP_ID || - !env.CHARLIE_APP_PRIVATE_KEY || - !env.CHARLIE_APP_INSTALLATION_ID - ) { - return null; - } - - return new Octokit({ - authStrategy: createAppAuth, - auth: { - appId: env.CHARLIE_APP_ID, - privateKey: env.CHARLIE_APP_PRIVATE_KEY.replace(/\\n/g, "\n"), - installationId: env.CHARLIE_APP_INSTALLATION_ID, - }, - }); -} - -async function createGitHubIssue( - title: string, - body: string, - labels: string[], -): Promise<{ url: string; number: number } | { error: string }> { - const octokit = getGitHubClient(); - if (!octokit) { - return { error: "GitHub App credentials not configured" }; - } - - try { - const response = await octokit.issues.create({ - owner: "fastrepl", - repo: "hyprnote", - title, - body, - labels, - }); - - return { - url: response.data.html_url, - number: response.data.number, - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - return { error: `GitHub API error: ${errorMessage}` }; - } -} - -async function addCommentToIssue( - issueNumber: number, - comment: string, -): Promise { - const octokit = getGitHubClient(); - if (!octokit) { - return; - } - - try { - await octokit.issues.createComment({ - owner: "fastrepl", - repo: "hyprnote", - issue_number: issueNumber, - body: comment, - }); - } catch { - // Silently fail for comment creation - } -} - -export const feedback = new Hono(); - -feedback.post( - "/submit", - describeRoute({ - tags: [API_TAGS.PRIVATE_SKIP_OPENAPI], - responses: { - 200: { - description: "Feedback submitted successfully", - content: { - "application/json": { - schema: resolver(FeedbackResponseSchema), - }, - }, - }, - 400: { - description: "Invalid request", - content: { - "application/json": { - schema: resolver(FeedbackResponseSchema), - }, - }, - }, - 500: { - description: "Server error", - content: { - "application/json": { - schema: resolver(FeedbackResponseSchema), - }, - }, - }, - }, - }), - validator("json", FeedbackRequestSchema), - async (c) => { - const { description, logs, deviceInfo } = c.req.valid("json"); - - const trimmedDescription = description.trim(); - const firstLine = trimmedDescription.split("\n")[0].slice(0, 100).trim(); - const title = firstLine || "Feedback"; - - const deviceInfoSection = [ - `**Platform:** ${deviceInfo.platform}`, - `**Architecture:** ${deviceInfo.arch}`, - `**OS Version:** ${deviceInfo.osVersion}`, - `**App Version:** ${deviceInfo.appVersion}`, - ].join("\n"); - - const body = `## Description -${trimmedDescription} - -## Device Information -${deviceInfoSection} - ---- -*This issue was submitted from the Hyprnote desktop app.* -`; - - const labels = ["product/desktop"]; - const result = await createGitHubIssue(title, body, labels); - - if ("error" in result) { - return c.json({ success: false, error: result.error }, 500); - } - - if (logs) { - const logSummary = await analyzeLogsWithAI(logs); - const logComment = `## Log Analysis - -${logSummary?.trim() ? `### Summary\n\`\`\`\n${logSummary}\n\`\`\`` : "_No errors or warnings found._"} - -
-Raw Logs (last 10KB) - -\`\`\` -${logs.slice(-10000)} -\`\`\` - -
`; - - await addCommentToIssue(result.number, logComment); - } - - return c.json({ success: true, issueUrl: result.url }, 200); - }, -); diff --git a/apps/api/src/routes/file-transcription.ts b/apps/api/src/routes/file-transcription.ts deleted file mode 100644 index 54c6094d12..0000000000 --- a/apps/api/src/routes/file-transcription.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { IngressWorkflowClient } from "@restatedev/restate-sdk-clients"; -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { resolver, validator } from "hono-openapi/zod"; -import { z } from "zod"; - -import type { AppBindings } from "../hono-bindings"; -import { getRestateClient } from "../integration/restate"; -import { supabaseAuthMiddleware } from "../middleware/supabase"; -import { API_TAGS } from "./constants"; - -const PipelineStatus = z.enum([ - "QUEUED", - "TRANSCRIBING", - "TRANSCRIBED", - "LLM_RUNNING", - "DONE", - "ERROR", -]); - -const StatusResponseSchema = z.object({ - status: PipelineStatus, - transcript: z.string().optional(), - llmResult: z.string().optional(), - error: z.string().optional(), -}); - -type StatusStateType = z.infer; - -type SttFileInput = { - userId: string; - fileId: string; -}; - -type SttFileDefinition = { - run: (ctx: unknown, input: SttFileInput) => Promise; - getStatus: (ctx: unknown) => Promise; -}; - -type SttFileClient = IngressWorkflowClient; - -const StartInputSchema = z.object({ - fileId: z.string(), - pipelineId: z.string().optional(), -}); - -const StartResponseSchema = z.object({ - pipelineId: z.string(), - invocationId: z.string(), -}); - -const StatusInputSchema = z.object({ - pipelineId: z.string(), -}); - -export const fileTranscription = new Hono(); - -fileTranscription.post( - "/start", - describeRoute({ - tags: [API_TAGS.PRIVATE], - security: [{ Bearer: [] }], - responses: { - 200: { - description: "Pipeline started", - content: { - "application/json": { - schema: resolver(StartResponseSchema), - }, - }, - }, - 400: { description: "Invalid fileId" }, - 401: { description: "Unauthorized" }, - 500: { description: "Internal error" }, - }, - }), - supabaseAuthMiddleware, - validator("json", StartInputSchema), - async (c) => { - const userId = c.get("supabaseUserId")!; - const data = c.req.valid("json"); - - const segments = data.fileId.split("/").filter(Boolean); - const [ownerId, ...rest] = segments; - - if ( - !ownerId || - ownerId !== userId || - rest.length === 0 || - rest.some((s) => s === "." || s === "..") - ) { - return c.json({ error: "Invalid fileId" }, 400); - } - - const safeFileId = `${userId}/${rest.join("/")}`; - const rawId = data.pipelineId ?? crypto.randomUUID(); - const pipelineId = `${userId}:${rawId}`; - - try { - const restateClient = getRestateClient(); - const workflowClient: SttFileClient = - restateClient.workflowClient( - { name: "SttFile" }, - pipelineId, - ); - const handle = await workflowClient.workflowSubmit({ - userId, - fileId: safeFileId, - }); - - return c.json({ - pipelineId, - invocationId: handle.invocationId, - }); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Unknown error"; - return c.json({ error: errorMessage }, 500); - } - }, -); - -fileTranscription.get( - "/status/:pipelineId", - describeRoute({ - tags: [API_TAGS.PRIVATE], - security: [{ Bearer: [] }], - responses: { - 200: { - description: "Pipeline status", - content: { - "application/json": { - schema: resolver(StatusResponseSchema), - }, - }, - }, - 401: { description: "Unauthorized" }, - 403: { description: "Forbidden" }, - 500: { description: "Internal error" }, - }, - }), - supabaseAuthMiddleware, - validator("param", StatusInputSchema), - async (c) => { - const userId = c.get("supabaseUserId")!; - const { pipelineId } = c.req.valid("param"); - - const [ownerId] = pipelineId.split(":"); - if (ownerId !== userId) { - return c.json({ error: "Forbidden" }, 403); - } - - try { - const restateClient = getRestateClient(); - const workflowClient: SttFileClient = - restateClient.workflowClient( - { name: "SttFile" }, - pipelineId, - ); - const status = await workflowClient.getStatus(); - - return c.json(status); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Unknown error"; - return c.json({ error: errorMessage }, 500); - } - }, -); - -fileTranscription.get( - "/result/:pipelineId", - describeRoute({ - tags: [API_TAGS.PRIVATE], - security: [{ Bearer: [] }], - responses: { - 200: { - description: "Pipeline result", - content: { - "application/json": { - schema: resolver(StatusResponseSchema), - }, - }, - }, - 401: { description: "Unauthorized" }, - 403: { description: "Forbidden" }, - 500: { description: "Internal error" }, - }, - }), - supabaseAuthMiddleware, - validator("param", StatusInputSchema), - async (c) => { - const userId = c.get("supabaseUserId")!; - const { pipelineId } = c.req.valid("param"); - - const [ownerId] = pipelineId.split(":"); - if (ownerId !== userId) { - return c.json({ error: "Forbidden" }, 403); - } - - try { - const restateClient = getRestateClient(); - const workflowClient: SttFileClient = - restateClient.workflowClient( - { name: "SttFile" }, - pipelineId, - ); - const result = await workflowClient.workflowAttach(); - - return c.json(result); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Unknown error"; - return c.json({ error: errorMessage }, 500); - } - }, -); diff --git a/apps/api/src/routes/health.ts b/apps/api/src/routes/health.ts deleted file mode 100644 index 55bf2d2b62..0000000000 --- a/apps/api/src/routes/health.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { resolver } from "hono-openapi/zod"; -import { z } from "zod"; - -import type { AppBindings } from "../hono-bindings"; -import { API_TAGS } from "./constants"; - -const HealthResponseSchema = z.object({ - status: z.string(), -}); - -export const health = new Hono(); - -health.get( - "/", - describeRoute({ - tags: [API_TAGS.PRIVATE_SKIP_OPENAPI], - responses: { - 200: { - description: "result", - content: { - "application/json": { - schema: resolver(HealthResponseSchema), - }, - }, - }, - }, - }), - (c) => c.json({ status: "ok" }, 200), -); diff --git a/apps/api/src/routes/index.ts b/apps/api/src/routes/index.ts deleted file mode 100644 index 6d90c684a7..0000000000 --- a/apps/api/src/routes/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Hono } from "hono"; - -import type { AppBindings } from "../hono-bindings"; -import { billing } from "./billing"; -import { feedback } from "./feedback"; -import { fileTranscription } from "./file-transcription"; -import { health } from "./health"; -import { rpc } from "./rpc"; -import { webhook } from "./webhook"; - -export { API_TAGS } from "./constants"; - -export const routes = new Hono(); - -routes.route("/health", health); -routes.route("/billing", billing); -routes.route("/feedback", feedback); -routes.route("/file-transcription", fileTranscription); -routes.route("/rpc", rpc); -routes.route("/webhook", webhook); diff --git a/apps/api/src/routes/rpc.ts b/apps/api/src/routes/rpc.ts deleted file mode 100644 index 4ae49a7731..0000000000 --- a/apps/api/src/routes/rpc.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { resolver } from "hono-openapi/zod"; -import { z } from "zod"; - -import type { AppBindings } from "../hono-bindings"; -import { supabaseAuthMiddleware } from "../middleware/supabase"; -import { API_TAGS } from "./constants"; - -const CanStartTrialResponseSchema = z.object({ - canStartTrial: z.boolean(), - reason: z - .enum(["eligible", "has_active_subscription", "had_recent_trial", "error"]) - .optional(), -}); - -export const rpc = new Hono(); - -rpc.get( - "/can-start-trial", - describeRoute({ - tags: [API_TAGS.PRIVATE], - responses: { - 200: { - description: "result", - content: { - "application/json": { - schema: resolver(CanStartTrialResponseSchema), - }, - }, - }, - }, - }), - supabaseAuthMiddleware, - async (c) => { - const supabase = c.get("supabaseClient"); - - if (!supabase) { - console.error("supabaseClient not attached by middleware"); - return c.json({ error: "Internal server error" }, 500); - } - - const { data, error } = await supabase.rpc("can_start_trial"); - - if (error) { - console.error("can_start_trial RPC failed:", error); - return c.json({ canStartTrial: false, reason: "error" }); - } - - const reason = data ? "eligible" : "has_active_subscription"; - return c.json({ canStartTrial: data as boolean, reason }); - }, -); diff --git a/apps/api/src/routes/webhook.ts b/apps/api/src/routes/webhook.ts deleted file mode 100644 index 74267a7140..0000000000 --- a/apps/api/src/routes/webhook.ts +++ /dev/null @@ -1,146 +0,0 @@ -import * as Sentry from "@sentry/bun"; -import { Hono } from "hono"; -import { describeRoute } from "hono-openapi"; -import { resolver } from "hono-openapi/zod"; -import { z } from "zod"; - -import { env } from "../env"; -import type { AppBindings } from "../hono-bindings"; -import { classifyContactStatus, getContactByEmail } from "../integration/loops"; -import { postThreadReply } from "../integration/slack"; -import { API_TAGS } from "./constants"; - -const WebhookSuccessSchema = z.object({ - ok: z.boolean(), -}); - -export const webhook = new Hono(); - -const SlackEventSchema = z.object({ - type: z.string(), - challenge: z.string().optional(), - event: z - .object({ - type: z.string(), - channel: z.string().optional(), - ts: z.string().optional(), - text: z.string().optional(), - bot_id: z.string().optional(), - user: z.string().optional(), - }) - .optional(), -}); - -function extractEmailFromLoopsMessage(text: string): string | null { - const mailtoMatch = text.match(/ { - const rawBody = c.get("slackRawBody"); - const span = c.get("sentrySpan"); - - console.log("[slack/events] Received request, rawBody:", rawBody); - - let payload: z.infer; - try { - payload = SlackEventSchema.parse(JSON.parse(rawBody)); - } catch (e) { - console.log("[slack/events] Failed to parse payload:", e); - return c.json({ error: "invalid_payload" }, 400); - } - - console.log("[slack/events] Parsed payload type:", payload.type); - - if (payload.type === "url_verification" && payload.challenge) { - console.log("[slack/events] URL verification, returning challenge"); - return c.json({ challenge: payload.challenge }, 200); - } - - if (payload.type !== "event_callback" || !payload.event) { - return c.json({ ok: true }, 200); - } - - const event = payload.event; - span?.setAttribute("slack.event_type", event.type); - - if (event.type !== "message" || !event.bot_id) { - return c.json({ ok: true }, 200); - } - - if ( - env.LOOPS_SLACK_CHANNEL_ID && - event.channel !== env.LOOPS_SLACK_CHANNEL_ID - ) { - return c.json({ ok: true }, 200); - } - - if ( - !event.text || - !event.text.includes("was added to your account") || - !event.ts || - !event.channel - ) { - return c.json({ ok: true }, 200); - } - - const email = extractEmailFromLoopsMessage(event.text); - if (!email) { - return c.json({ ok: true }, 200); - } - - try { - const contact = await getContactByEmail(email); - if (!contact) { - return c.json({ ok: true }, 200); - } - - const status = classifyContactStatus(contact); - const source = contact.source || "Unknown"; - const details = [ - `Source: ${source}`, - contact.intent ? `Intent: ${contact.intent}` : null, - contact.platform ? `Platform: ${contact.platform}` : null, - ] - .filter(Boolean) - .join(", "); - - await postThreadReply( - event.channel, - event.ts, - `Status: ${status} (${details})`, - ); - } catch (error) { - Sentry.captureException(error, { - tags: { webhook: "slack", step: "loops_source_thread" }, - extra: { email }, - }); - return c.json({ error: "failed_to_process" }, 500); - } - - return c.json({ ok: true }, 200); - }, -); diff --git a/apps/api/src/scripts/generate-openapi.ts b/apps/api/src/scripts/generate-openapi.ts deleted file mode 100644 index e9036f0468..0000000000 --- a/apps/api/src/scripts/generate-openapi.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createClient } from "@hey-api/openapi-ts"; -import { generateSpecs } from "hono-openapi"; - -import { openAPIDocumentation } from "../openapi"; -import { routes } from "../routes"; -import { API_TAGS } from "../routes/constants"; - -async function main() { - const specs = await generateSpecs(routes, { - documentation: openAPIDocumentation, - }); - - const outputPath = new URL("../../openapi.gen.json", import.meta.url); - await Bun.write(outputPath, JSON.stringify(specs, null, 2)); - console.log(`OpenAPI spec written to ${outputPath.pathname}`); - - try { - await createClient({ - input: "./openapi.gen.json", - output: "../../packages/api-client/src/generated", - parser: { - filters: { - tags: { - include: [API_TAGS.PRIVATE], - }, - }, - }, - }); - console.log("OpenAPI client generated successfully"); - } catch (error) { - console.error(error); - process.exit(1); - } -} - -void main(); diff --git a/apps/api/src/scripts/stripe-backfill-entitlements.ts b/apps/api/src/scripts/stripe-backfill-entitlements.ts deleted file mode 100644 index a09d6e075f..0000000000 --- a/apps/api/src/scripts/stripe-backfill-entitlements.ts +++ /dev/null @@ -1,205 +0,0 @@ -// Backfill entitlements for customers who subscribed before entitlements were properly set up. -// Unlike stripe-sync-entitlements.ts which relies on Stripe's entitlements API, -// this script manually calculates entitlements based on subscription state. -// -// Stripe's entitlements API only updates at subscription creation or billing cycle, -// so past customers need this backfill. -import { Effect, Schedule } from "effect"; -import pg from "pg"; -import { parseArgs } from "util"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "dry-run": { - type: "boolean", - default: false, - }, - }, - strict: true, - allowPositionals: false, -}); - -const dryRun = values["dry-run"] ?? false; - -const { DATABASE_URL } = Bun.env; - -if (!DATABASE_URL) { - throw new Error("Missing required DATABASE_URL environment variable"); -} - -const pool = new pg.Pool({ connectionString: DATABASE_URL }); - -const PRODUCT_ID = "prod_SHWUtH1i2DPvSD"; -const ENTITLEMENT_LOOKUP_KEY = "hyprnote_pro"; - -class DbError { - readonly _tag = "DbError"; - constructor(readonly message: string) {} -} - -const retryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.intersect(Schedule.recurs(3)), -); - -const fetchCustomersWithActiveSubscriptions = Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT DISTINCT s.customer - FROM stripe.subscriptions s - JOIN stripe.subscription_items si ON si.subscription = s.id - JOIN stripe.prices p ON si.price = p.id - WHERE s.status IN ('active', 'trialing', 'past_due') - AND p.product = $1 - AND s.customer IS NOT NULL`, - [PRODUCT_ID], - ), - catch: (e) => - new DbError( - `Failed to fetch subscriptions: ${e instanceof Error ? e.message : String(e)}`, - ), -}).pipe(Effect.map((result) => new Set(result.rows.map((r) => r.customer)))); - -const fetchCustomersWithEntitlements = Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT DISTINCT customer FROM stripe.active_entitlements WHERE lookup_key = $1`, - [ENTITLEMENT_LOOKUP_KEY], - ), - catch: (e) => - new DbError( - `Failed to fetch entitlements: ${e instanceof Error ? e.message : String(e)}`, - ), -}).pipe(Effect.map((result) => new Set(result.rows.map((r) => r.customer)))); - -const createEntitlement = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query( - `INSERT INTO stripe.active_entitlements (id, object, livemode, feature, customer, lookup_key, last_synced_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (customer, lookup_key) DO UPDATE SET - last_synced_at = EXCLUDED.last_synced_at`, - [ - `backfill_${customerId}_${ENTITLEMENT_LOOKUP_KEY}`, - "entitlements.active_entitlement", - true, - null, - customerId, - ENTITLEMENT_LOOKUP_KEY, - new Date().toISOString(), - ], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.retry(retryPolicy), - Effect.map(() => true), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to create entitlement for ${customerId}: ${e.message}`, - ); - return false; - }), - ), - ); - -const deleteEntitlement = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query( - `DELETE FROM stripe.active_entitlements WHERE customer = $1 AND lookup_key = $2`, - [customerId, ENTITLEMENT_LOOKUP_KEY], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.retry(retryPolicy), - Effect.map(() => true), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to delete entitlement for ${customerId}: ${e.message}`, - ); - return false; - }), - ), - ); - -const program = Effect.gen(function* () { - yield* Effect.log( - `Starting entitlement backfill${dryRun ? " (DRY RUN)" : ""}...`, - ); - yield* Effect.log(`Product ID: ${PRODUCT_ID}`); - yield* Effect.log(`Entitlement lookup_key: ${ENTITLEMENT_LOOKUP_KEY}`); - - const [activeCustomers, entitledCustomers] = yield* Effect.all([ - fetchCustomersWithActiveSubscriptions, - fetchCustomersWithEntitlements, - ]); - - yield* Effect.log( - `Found ${activeCustomers.size} customers with active subscriptions`, - ); - yield* Effect.log( - `Found ${entitledCustomers.size} customers with existing entitlements`, - ); - - const toAdd = [...activeCustomers].filter((c) => !entitledCustomers.has(c)); - const toRemove = [...entitledCustomers].filter( - (c) => !activeCustomers.has(c), - ); - - yield* Effect.log(`Customers needing entitlements added: ${toAdd.length}`); - yield* Effect.log( - `Customers needing entitlements removed: ${toRemove.length}`, - ); - - if (dryRun) { - if (toAdd.length > 0) { - yield* Effect.log(`Would add entitlements for: ${toAdd.join(", ")}`); - } - if (toRemove.length > 0) { - yield* Effect.log( - `Would remove entitlements for: ${toRemove.join(", ")}`, - ); - } - yield* Effect.log("Dry run complete - no changes made"); - return; - } - - let added = 0; - let removed = 0; - let errors = 0; - - for (const customerId of toAdd) { - const success = yield* createEntitlement(customerId); - if (success) { - added++; - } else { - errors++; - } - } - - for (const customerId of toRemove) { - const success = yield* deleteEntitlement(customerId); - if (success) { - removed++; - } else { - errors++; - } - } - - yield* Effect.log( - `Backfill complete: added=${added}, removed=${removed}, errors=${errors}`, - ); -}); - -Effect.runPromise(program) - .catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }) - .finally(() => { - pool.end(); - }); diff --git a/apps/api/src/scripts/stripe-backfill-features.ts b/apps/api/src/scripts/stripe-backfill-features.ts deleted file mode 100644 index fa004a8836..0000000000 --- a/apps/api/src/scripts/stripe-backfill-features.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Sync features from Stripe's entitlements API to the local database. -// Features define what entitlements can be granted to customers. -// https://docs.stripe.com/api/entitlements/feature/list -import { Effect, Schedule } from "effect"; -import pg from "pg"; -import Stripe from "stripe"; -import { parseArgs } from "util"; - -import { STRIPE_API_VERSION } from "../integration/stripe"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "dry-run": { - type: "boolean", - default: false, - }, - }, - strict: true, - allowPositionals: false, -}); - -const dryRun = values["dry-run"] ?? false; - -const { STRIPE_SECRET_KEY, DATABASE_URL } = Bun.env; - -if (!STRIPE_SECRET_KEY || !DATABASE_URL) { - throw new Error( - "Missing required STRIPE_SECRET_KEY or DATABASE_URL environment variables", - ); -} - -const pool = new pg.Pool({ connectionString: DATABASE_URL }); -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, -}); - -class DbError { - readonly _tag = "DbError"; - constructor(readonly message: string) {} -} - -const isRateLimitError = (error: unknown): boolean => - error instanceof Stripe.errors.StripeError && error.code === "rate_limit"; - -const retryPolicy = Schedule.exponential("500 millis").pipe( - Schedule.jittered, - Schedule.whileInput(isRateLimitError), - Schedule.intersect(Schedule.recurs(5)), -); - -const fetchFeaturesFromStripe = Effect.tryPromise({ - try: async () => { - const features: Stripe.Entitlements.Feature[] = []; - for await (const feature of stripe.entitlements.features.list()) { - features.push(feature); - } - return features; - }, - catch: (error) => error, -}).pipe(Effect.retry(retryPolicy)); - -const upsertFeature = (feature: Stripe.Entitlements.Feature) => - Effect.tryPromise({ - try: () => - pool.query( - `INSERT INTO stripe.features (id, object, livemode, name, lookup_key, active, metadata, last_synced_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - ON CONFLICT (id) DO UPDATE SET - object = EXCLUDED.object, - livemode = EXCLUDED.livemode, - name = EXCLUDED.name, - lookup_key = EXCLUDED.lookup_key, - active = EXCLUDED.active, - metadata = EXCLUDED.metadata, - last_synced_at = EXCLUDED.last_synced_at`, - [ - feature.id, - feature.object, - feature.livemode, - feature.name, - feature.lookup_key, - feature.active, - JSON.stringify(feature.metadata || {}), - new Date().toISOString(), - ], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.map(() => true), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to upsert feature ${feature.id}: ${e.message}`, - ); - return false; - }), - ), - ); - -const program = Effect.gen(function* () { - yield* Effect.log( - `Starting Stripe features sync${dryRun ? " (DRY RUN)" : ""}...`, - ); - - const features = yield* fetchFeaturesFromStripe; - - yield* Effect.log(`Found ${features.length} features in Stripe`); - - if (features.length === 0) { - yield* Effect.log("No features found in Stripe. Nothing to sync."); - return; - } - - for (const feature of features) { - yield* Effect.log( - ` - ${feature.lookup_key}: ${feature.name} (active: ${feature.active})`, - ); - } - - if (dryRun) { - yield* Effect.log("Dry run complete - no changes made"); - return; - } - - let synced = 0; - let errors = 0; - - for (const feature of features) { - const success = yield* upsertFeature(feature); - if (success) { - synced++; - } else { - errors++; - } - } - - yield* Effect.log(`Sync complete: synced=${synced}, errors=${errors}`); -}); - -Effect.runPromise(program) - .catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }) - .finally(() => { - pool.end(); - }); diff --git a/apps/api/src/scripts/stripe-sync-entitlements.ts b/apps/api/src/scripts/stripe-sync-entitlements.ts deleted file mode 100644 index 5eb88c106e..0000000000 --- a/apps/api/src/scripts/stripe-sync-entitlements.ts +++ /dev/null @@ -1,317 +0,0 @@ -// https://github.com/supabase/stripe-sync-engine/blob/main/packages/sync-engine/README.md#syncing-a-single-entity -// Entitlements can not be synced with "stripe-sync-engine". So we need this script. -// -// Syncs entitlements for customers that are "worth looking at": -// 1. Customers with active/trialing/past_due subscriptions (should have entitlements) -// 2. Customers with existing entitlements (might need updates or cleanup) -// -// This handles both backfill (pre-webhook customers) and daily verification. -import { Effect, Schedule } from "effect"; -import pg from "pg"; -import Stripe from "stripe"; -import { parseArgs } from "util"; - -import { STRIPE_API_VERSION } from "../integration/stripe"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - "skip-recent-hours": { - type: "string", - default: "6", - }, - }, - strict: true, - allowPositionals: false, -}); - -const skipRecentHours = parseInt(values["skip-recent-hours"] ?? "6", 10); - -const { STRIPE_SECRET_KEY, DATABASE_URL } = Bun.env; - -if (!STRIPE_SECRET_KEY || !DATABASE_URL) { - throw new Error("Missing required environment variables"); -} - -const pool = new pg.Pool({ connectionString: DATABASE_URL }); -const stripe = new Stripe(STRIPE_SECRET_KEY, { - apiVersion: STRIPE_API_VERSION, -}); - -const isRateLimitError = (error: unknown): boolean => - error instanceof Stripe.errors.StripeError && error.code === "rate_limit"; - -const retryPolicy = Schedule.exponential("500 millis").pipe( - Schedule.jittered, - Schedule.whileInput(isRateLimitError), - Schedule.intersect(Schedule.recurs(5)), -); - -class DbError { - readonly _tag = "DbError"; - constructor(readonly message: string) {} -} - -const fetchRecentlySyncedCustomers = (hours: number) => - Effect.gen(function* () { - if (hours <= 0) return new Set(); - - const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString(); - - const result = yield* Effect.tryPromise({ - try: () => - pool.query<{ id: string }>( - `SELECT id FROM stripe.customers WHERE last_synced_at >= $1`, - [cutoff], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logWarning( - `Failed to fetch recently synced customers: ${e.message}`, - ); - return { rows: [] as { id: string }[] }; - }), - ), - ); - - return new Set(result.rows.map((c) => c.id)); - }); - -const fetchCustomersToSync = Effect.gen(function* () { - const [subscriptionsResult, entitlementsResult, recentlySynced] = - yield* Effect.all([ - Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT customer FROM stripe.subscriptions WHERE status IN ('active', 'trialing', 'past_due')`, - ), - catch: (e) => - new DbError( - `Failed to fetch subscriptions: ${e instanceof Error ? e.message : String(e)}`, - ), - }), - Effect.tryPromise({ - try: () => - pool.query<{ customer: string }>( - `SELECT customer FROM stripe.active_entitlements`, - ), - catch: (e) => - new DbError( - `Failed to fetch existing entitlements: ${e instanceof Error ? e.message : String(e)}`, - ), - }), - fetchRecentlySyncedCustomers(skipRecentHours), - ]); - - const uniqueIds = new Set([ - ...subscriptionsResult.rows.map((s) => s.customer).filter(Boolean), - ...entitlementsResult.rows.map((e) => e.customer).filter(Boolean), - ]); - - const filtered = Array.from(uniqueIds).filter( - (id) => !recentlySynced.has(id), - ); - const skipped = uniqueIds.size - filtered.length; - - if (skipped > 0) { - yield* Effect.log( - `Skipping ${skipped} customers synced within the last ${skipRecentHours} hours`, - ); - } - - return filtered; -}); - -const fetchCustomerEntitlements = (customerId: string) => - Effect.tryPromise({ - try: async () => { - const entitlements: Stripe.Entitlements.ActiveEntitlement[] = []; - for await (const entitlement of stripe.entitlements.activeEntitlements.list( - { - customer: customerId, - }, - )) { - entitlements.push(entitlement); - } - return entitlements; - }, - catch: (error) => error, - }).pipe(Effect.retry(retryPolicy)); - -const deleteAllEntitlements = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query(`DELETE FROM stripe.active_entitlements WHERE customer = $1`, [ - customerId, - ]), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.map((result) => ({ - updated: 0, - deleted: result.rowCount ?? 0, - hasError: false, - })), - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to delete entitlements for ${customerId}: ${e.message}`, - ); - return { updated: 0, deleted: 0, hasError: true }; - }), - ), - ); - -const syncEntitlements = ( - customerId: string, - entitlements: Stripe.Entitlements.ActiveEntitlement[], -) => - Effect.gen(function* () { - const activeLookupKeys = entitlements.map((e) => e.lookup_key); - - const deleteResult = yield* Effect.tryPromise({ - try: () => - pool.query( - `DELETE FROM stripe.active_entitlements WHERE customer = $1 AND lookup_key != ALL($2)`, - [customerId, activeLookupKeys], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to delete stale entitlements for ${customerId}: ${e.message}`, - ); - return null; - }), - ), - ); - - if (deleteResult === null) { - return { updated: 0, deleted: 0, hasError: true }; - } - - const deleteCount = deleteResult.rowCount ?? 0; - - for (const entitlement of entitlements) { - const upsertResult = yield* Effect.tryPromise({ - try: () => - pool.query( - `INSERT INTO stripe.active_entitlements (id, object, livemode, feature, customer, lookup_key, last_synced_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (customer, lookup_key) DO UPDATE SET - id = EXCLUDED.id, - object = EXCLUDED.object, - livemode = EXCLUDED.livemode, - feature = EXCLUDED.feature, - last_synced_at = EXCLUDED.last_synced_at`, - [ - entitlement.id, - entitlement.object, - entitlement.livemode, - entitlement.feature, - customerId, - entitlement.lookup_key, - new Date().toISOString(), - ], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to upsert entitlement for ${customerId}: ${e.message}`, - ); - return null; - }), - ), - ); - - if (upsertResult === null) { - return { updated: 0, deleted: deleteCount, hasError: true }; - } - } - - return { - updated: entitlements.length, - deleted: deleteCount, - hasError: false, - }; - }); - -const updateCustomerLastSyncedAt = (customerId: string) => - Effect.tryPromise({ - try: () => - pool.query( - `UPDATE stripe.customers SET last_synced_at = $1 WHERE id = $2`, - [new Date().toISOString(), customerId], - ), - catch: (e) => new DbError(e instanceof Error ? e.message : String(e)), - }).pipe(Effect.catchAll(() => Effect.void)); - -const processCustomer = (customerId: string) => - Effect.gen(function* () { - const entitlements = yield* fetchCustomerEntitlements(customerId); - - const result = - entitlements.length === 0 - ? yield* deleteAllEntitlements(customerId) - : yield* syncEntitlements(customerId, entitlements); - - if (!result.hasError) { - yield* updateCustomerLastSyncedAt(customerId); - } - - return result; - }).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError( - `Failed to process customer ${customerId}: ${error}`, - ); - return { updated: 0, deleted: 0, hasError: true }; - }), - ), - ); - -const program = Effect.gen(function* () { - yield* Effect.log("Starting Stripe entitlements sync..."); - yield* Effect.log( - "Fetching customers with active subscriptions or existing entitlements...", - ); - - const customerIds = yield* fetchCustomersToSync; - - yield* Effect.log(`Found ${customerIds.length} customers to process`); - - let processed = 0; - let totalUpdated = 0; - let totalDeleted = 0; - let totalErrors = 0; - - for (const customerId of customerIds) { - const result = yield* processCustomer(customerId); - processed++; - totalUpdated += result.updated ?? 0; - totalDeleted += result.deleted; - if (result.hasError) totalErrors++; - - if (processed % 100 === 0) { - yield* Effect.log(`Progress: ${processed}/${customerIds.length}`); - } - } - - yield* Effect.log( - `Sync complete: processed=${processed}, updated=${totalUpdated}, deleted=${totalDeleted}, errors=${totalErrors}`, - ); -}); - -Effect.runPromise(program) - .catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }) - .finally(() => { - pool.end(); - }); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json deleted file mode 100644 index 44907d8c5f..0000000000 --- a/apps/api/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "include": ["src/**/*.ts"], - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "strict": true, - "skipLibCheck": true, - "noUncheckedSideEffectImports": true, - "types": ["@types/bun"] - } -} diff --git a/apps/web/content/docs/developers/1.env.mdx b/apps/web/content/docs/developers/1.env.mdx index eb4f805336..31718d9946 100644 --- a/apps/web/content/docs/developers/1.env.mdx +++ b/apps/web/content/docs/developers/1.env.mdx @@ -42,11 +42,11 @@ S3_SECRET_KEY="850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907" S3_REGION="local" ``` -See `apps/web/package.json`, `apps/api/package.json`, and `apps/restate/package.json` to see how `.env.supabase` is used. +See `apps/web/package.json` and `apps/restate/package.json` to see how `.env.supabase` is used. -# AI Server (apps/ai) +# AI Server (apps/api) -The AI server uses the `envy` crate for structured environment configuration. Create a `.env` file in `apps/ai/` with: +The AI server uses the `envy` crate for structured environment configuration. Create a `.env` file in `apps/api/` with: ```bash PORT=3001 # default: 3001 diff --git a/apps/web/content/docs/developers/5.run.mdx b/apps/web/content/docs/developers/5.run.mdx index 10b4c5676c..5536c8f316 100644 --- a/apps/web/content/docs/developers/5.run.mdx +++ b/apps/web/content/docs/developers/5.run.mdx @@ -31,9 +31,10 @@ pnpm -F web dev ## API (apps/api) ```bash -pnpm -F api dev +cargo run -p api ``` + # Supabase - https://supabase.com/docs/guides/local-development/cli/getting-started diff --git a/bacon.toml b/bacon.toml index 8910750644..92054a9b55 100644 --- a/bacon.toml +++ b/bacon.toml @@ -1,13 +1,13 @@ -default_job = "ai" +default_job = "api" -[jobs.ai] -command = ["cargo", "run", "-p", "ai"] +[jobs.api] +command = ["cargo", "run", "-p", "api"] need_stdout = true on_change_strategy = "kill_then_restart" allow_failures = true default_watch = false watch = [ - "apps/ai/src", + "apps/api/src", "crates/analytics/src", "crates/llm-proxy/src", "crates/supabase-auth/src", diff --git a/doxxer.ai.toml b/doxxer.ai.toml deleted file mode 100644 index ad1f78e3b8..0000000000 --- a/doxxer.ai.toml +++ /dev/null @@ -1,13 +0,0 @@ -filter.tag = "^ai_v" - -[output] -format = "plain" - -[next.patch] -increment = 1 - -[next.minor] -increment = 1 - -[next.major] -increment = 1 diff --git a/packages/api-client2/openapi-ts.config.ts b/packages/api-client2/openapi-ts.config.ts index d97fe0a3bb..4a85dd83e9 100644 --- a/packages/api-client2/openapi-ts.config.ts +++ b/packages/api-client2/openapi-ts.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from "@hey-api/openapi-ts"; export default defineConfig({ - input: "../../apps/ai/openapi.gen.json", + input: "../../apps/api/openapi.gen.json", output: "src/generated", }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e7e7faf9b..707fa3f450 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,85 +42,6 @@ importers: specifier: ^8.54.0 version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - apps/api: - dependencies: - '@hono/zod-validator': - specifier: ^0.7.6 - version: 0.7.6(hono@4.11.7)(zod@4.3.6) - '@hypr/api-client': - specifier: workspace:* - version: link:../../packages/api-client - '@hypr/supabase': - specifier: workspace:* - version: link:../../packages/supabase - '@octokit/auth-app': - specifier: ^8.1.2 - version: 8.2.0 - '@octokit/rest': - specifier: ^22.0.1 - version: 22.0.1 - '@posthog/ai': - specifier: ^7.8.2 - version: 7.8.2(@ai-sdk/provider@3.0.8)(@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.3.6))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(posthog-node@5.24.7)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@4.3.6)) - '@restatedev/restate-sdk-clients': - specifier: ^1.10.2 - version: 1.10.2 - '@scalar/hono-api-reference': - specifier: ^0.5.184 - version: 0.5.184(hono@4.11.7) - '@sentry/bun': - specifier: ^10.38.0 - version: 10.38.0 - '@supabase/supabase-js': - specifier: ^2.93.3 - version: 2.93.3 - '@t3-oss/env-core': - specifier: ^0.13.10 - version: 0.13.10(typescript@5.9.3)(zod@4.3.6) - '@types/pg': - specifier: ^8.16.0 - version: 8.16.0 - effect: - specifier: ^3.19.15 - version: 3.19.15 - hono: - specifier: ^4.11.7 - version: 4.11.7 - hono-openapi: - specifier: ^0.4.8 - version: 0.4.8(@hono/zod-validator@0.7.6(hono@4.11.7)(zod@4.3.6))(@sinclair/typebox@0.34.48)(effect@3.19.15)(hono@4.11.7)(openapi-types@12.1.3)(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6) - openai: - specifier: ^6.17.0 - version: 6.17.0(ws@8.19.0)(zod@4.3.6) - pg: - specifier: ^8.18.0 - version: 8.18.0 - posthog-node: - specifier: ^5.24.7 - version: 5.24.7 - stripe: - specifier: ^19.3.1 - version: 19.3.1(@types/node@25.2.0) - zod: - specifier: ^4.3.6 - version: 4.3.6 - zod-openapi: - specifier: ^5.4.6 - version: 5.4.6(zod@4.3.6) - devDependencies: - '@dotenvx/dotenvx': - specifier: ^1.52.0 - version: 1.52.0 - '@hey-api/openapi-ts': - specifier: ^0.91.1 - version: 0.91.1(magicast@0.5.1)(typescript@5.9.3) - '@types/bun': - specifier: ^1.3.8 - version: 1.3.8 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - apps/bot: dependencies: '@t3-oss/env-core': @@ -2095,19 +2016,6 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.71.2': - resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@apidevtools/json-schema-ref-parser@11.9.3': - resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} - engines: {node: '>= 16'} - '@apm-js-collab/code-transformer@0.8.2': resolution: {integrity: sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==} @@ -3431,15 +3339,6 @@ packages: react: ^16 || ^17 || ^18 || ^19 react-dom: ^16 || ^17 || ^18 || ^19 - '@google/genai@1.39.0': - resolution: {integrity: sha512-Vz7AQsOdBeiIcxmXIQNy/hzDvyAOE1lSpWA10itUQza7h3aQFF6QSGaQ7o1GYsjMD3XslK4Ee/Ol0eLhRXb7gA==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - '@grpc/grpc-js@1.14.3': resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} engines: {node: '>=12.10.0'} @@ -4417,34 +4316,18 @@ packages: resolution: {integrity: sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==} engines: {node: '>= 18'} - '@octokit/auth-app@8.2.0': - resolution: {integrity: sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==} - engines: {node: '>= 20'} - '@octokit/auth-oauth-app@7.1.0': resolution: {integrity: sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==} engines: {node: '>= 18'} - '@octokit/auth-oauth-app@9.0.3': - resolution: {integrity: sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==} - engines: {node: '>= 20'} - '@octokit/auth-oauth-device@6.1.0': resolution: {integrity: sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==} engines: {node: '>= 18'} - '@octokit/auth-oauth-device@8.0.3': - resolution: {integrity: sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==} - engines: {node: '>= 20'} - '@octokit/auth-oauth-user@4.1.0': resolution: {integrity: sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==} engines: {node: '>= 18'} - '@octokit/auth-oauth-user@6.0.2': - resolution: {integrity: sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==} - engines: {node: '>= 20'} - '@octokit/auth-token@4.0.0': resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} @@ -4485,18 +4368,10 @@ packages: resolution: {integrity: sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==} engines: {node: '>= 18'} - '@octokit/oauth-authorization-url@8.0.0': - resolution: {integrity: sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==} - engines: {node: '>= 20'} - '@octokit/oauth-methods@4.1.0': resolution: {integrity: sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==} engines: {node: '>= 18'} - '@octokit/oauth-methods@6.0.2': - resolution: {integrity: sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==} - engines: {node: '>= 20'} - '@octokit/openapi-types@20.0.0': resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==} @@ -4525,12 +4400,6 @@ packages: peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-paginate-rest@14.0.0': - resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} - engines: {node: '>= 20'} - peerDependencies: - '@octokit/core': '>=6' - '@octokit/plugin-request-log@6.0.0': resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} engines: {node: '>= 20'} @@ -4587,10 +4456,6 @@ packages: resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==} engines: {node: '>= 20'} - '@octokit/rest@22.0.1': - resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} - engines: {node: '>= 20'} - '@octokit/types@12.6.0': resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==} @@ -5550,16 +5415,6 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@posthog/ai@7.8.2': - resolution: {integrity: sha512-GM9WN+rqfQ9ERI6RyJVJizNA0aEH4O5vTwCfmgrLaMKejvDu1kKpX2ZO10tXDIHM8z6ce2ixQbljdAQL9WvzRQ==} - engines: {node: ^20.20.0 || >=22.22.0} - peerDependencies: - '@ai-sdk/provider': ^2.0.0 || ^3.0.0 - posthog-node: ^5.0.0 - peerDependenciesMeta: - '@ai-sdk/provider': - optional: true - '@posthog/core@1.17.0': resolution: {integrity: sha512-8pDNL+/u9ojzXloA5wILVDXBCV5daJ7w2ipCALQlEEZmL752cCKhRpbyiHn3tjKXh3Hy6aOboJneYa1JdlVHrQ==} @@ -6559,9 +6414,6 @@ packages: cpu: [x64] os: [linux] - '@restatedev/restate-sdk-clients@1.10.2': - resolution: {integrity: sha512-LHk+WLvXxaS4+yrAjlOY3L64kt/NNhsFl3qt2tf50lmhBt66WZnbK7TqqiH09/+QoiYgCJeWByh615D8AufPOg==} - '@restatedev/restate-sdk-cloudflare-workers@1.10.2': resolution: {integrity: sha512-kkokxi5ZID/Sd8XefV/SrX9dFheqGAHz96BCGD70vyQhNhKemBTIvkmhdN5Acd9v/8OnApfCmSaYORB8Jqldiw==} @@ -6733,24 +6585,6 @@ packages: cpu: [x64] os: [win32] - '@scalar/core@0.1.1': - resolution: {integrity: sha512-7qnZp8ykrXoKScFIZcwt638CuFFyj7G3SsgVfD5liNgb533K8/lhWqdmp1vK2u4BKKJ9GBAPKMlWZE/+yA8WTw==} - engines: {node: '>=18'} - - '@scalar/hono-api-reference@0.5.184': - resolution: {integrity: sha512-vRSRwJkN1Xo5dW9KYQJlGpKZ+Nh9qH+x1sn0qf6/Lx8QLPyyEpNm1EEddKaIN6qd5wrtVjDN6adQhfAfcYGHzw==} - engines: {node: '>=18'} - peerDependencies: - hono: ^4.0.0 - - '@scalar/openapi-types@0.1.9': - resolution: {integrity: sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g==} - engines: {node: '>=18'} - - '@scalar/types@0.0.40': - resolution: {integrity: sha512-0J6o+yZzgZEvl3KhvLTAGiXXyrCeEPKvs9gUWQDf1Rb5NfFxF0lA10ougCQCwVJIguWNEzZfOUiSoAFzGy2EqQ==} - engines: {node: '>=18'} - '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -8569,9 +8403,6 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unhead/schema@1.11.20': - resolution: {integrity: sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==} - '@unpic/core@1.0.3': resolution: {integrity: sha512-aum9YNVUGso7MjGLD0Rp/08kywCGLqZ03/q6VQBFFakDBOXWEc8D4kPGcZ8v5wEnGRex3lE+++bOuucBp3KJ/w==} @@ -9522,10 +9353,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} @@ -11410,51 +11237,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono-openapi@0.4.8: - resolution: {integrity: sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA==} - peerDependencies: - '@hono/arktype-validator': ^2.0.0 - '@hono/effect-validator': ^1.2.0 - '@hono/typebox-validator': ^0.2.0 || ^0.3.0 - '@hono/valibot-validator': ^0.5.1 - '@hono/zod-validator': ^0.4.1 - '@sinclair/typebox': ^0.34.9 - '@valibot/to-json-schema': ^1.0.0-beta.3 - arktype: ^2.0.0 - effect: ^3.11.3 - hono: ^4.6.13 - openapi-types: ^12.1.3 - valibot: ^1.0.0-beta.9 - zod: ^3.23.8 - zod-openapi: ^4.0.0 - peerDependenciesMeta: - '@hono/arktype-validator': - optional: true - '@hono/effect-validator': - optional: true - '@hono/typebox-validator': - optional: true - '@hono/valibot-validator': - optional: true - '@hono/zod-validator': - optional: true - '@sinclair/typebox': - optional: true - '@valibot/to-json-schema': - optional: true - arktype: - optional: true - effect: - optional: true - hono: - optional: true - valibot: - optional: true - zod: - optional: true - zod-openapi: - optional: true - hono-rate-limiter@0.4.2: resolution: {integrity: sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw==} peerDependencies: @@ -11464,9 +11246,6 @@ packages: resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} engines: {node: '>=16.9.0'} - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -12051,10 +11830,6 @@ packages: json-schema-ref-resolver@1.0.1: resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -12064,10 +11839,6 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-schema-walker@2.0.0: - resolution: {integrity: sha512-nXN2cMky0Iw7Af28w061hmxaPDaML5/bQD9nwm1lOoIKEGjHcRGxqWe4MfrkYThYAPjSUhmsp4bJNoLAyVn9Xw==} - engines: {node: '>=10'} - json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -12168,12 +11939,6 @@ packages: engines: {node: '>=8'} hasBin: true - langchain@1.2.16: - resolution: {integrity: sha512-gVLSNhP8It6vDMribkqyKIiq79F2kCPAk3L4nGRUzKvIK2XD0J1y6UkuzEKwJ48w5rEKYHI729qthBteXQHOmA==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': 1.1.18 - langium@3.3.1: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} engines: {node: '>=16.0.0'} @@ -13395,9 +13160,6 @@ packages: openapi-fetch@0.15.0: resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==} - openapi-types@12.1.3: - resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} @@ -13926,10 +13688,6 @@ packages: resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==} engines: {node: '>=15.0.0'} - posthog-node@5.24.7: - resolution: {integrity: sha512-IJ0Zj+v+eg/JQMZ75n0Hcp4NzuQzWcZjqFjcUQs6RhW2l5FiQIq09sKJMleXX33hYxD6sfjFsDTqugJlgeAohg==} - engines: {node: ^20.20.0 || >=22.22.0} - powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -15621,9 +15379,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -15898,9 +15653,6 @@ packages: universal-github-app-jwt@1.2.0: resolution: {integrity: sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==} - universal-github-app-jwt@2.2.2: - resolution: {integrity: sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==} - universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} @@ -16684,19 +16436,10 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - zhead@2.2.4: - resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} - zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod-openapi@5.4.6: - resolution: {integrity: sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A==} - engines: {node: '>=20'} - peerDependencies: - zod: ^3.25.74 || ^4.0.0 - zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -16920,18 +16663,6 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 - '@anthropic-ai/sdk@0.71.2(zod@4.3.6)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.3.6 - - '@apidevtools/json-schema-ref-parser@11.9.3': - dependencies: - '@jsdevtools/ono': 7.1.3 - '@types/json-schema': 7.0.15 - js-yaml: 4.1.1 - '@apm-js-collab/code-transformer@0.8.2': {} '@apm-js-collab/tracing-hooks@0.3.1': @@ -18145,18 +17876,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@google/genai@1.39.0(@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.3.6))': - dependencies: - google-auth-library: 10.5.0 - protobufjs: 7.5.4 - ws: 8.19.0 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.3.6) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - '@grpc/grpc-js@1.14.3': dependencies: '@grpc/proto-loader': 0.8.0 @@ -19000,31 +18719,6 @@ snapshots: - hono - supports-color - '@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.3.6)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.7) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) - optionalDependencies: - '@cfworker/json-schema': 4.1.1 - transitivePeerDependencies: - - hono - - supports-color - optional: true - '@mswjs/interceptors@0.39.8': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -19817,17 +19511,6 @@ snapshots: universal-github-app-jwt: 1.2.0 universal-user-agent: 6.0.1 - '@octokit/auth-app@8.2.0': - dependencies: - '@octokit/auth-oauth-app': 9.0.3 - '@octokit/auth-oauth-user': 6.0.2 - '@octokit/request': 10.0.7 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - toad-cache: 3.7.0 - universal-github-app-jwt: 2.2.2 - universal-user-agent: 7.0.3 - '@octokit/auth-oauth-app@7.1.0': dependencies: '@octokit/auth-oauth-device': 6.1.0 @@ -19838,14 +19521,6 @@ snapshots: btoa-lite: 1.0.0 universal-user-agent: 6.0.1 - '@octokit/auth-oauth-app@9.0.3': - dependencies: - '@octokit/auth-oauth-device': 8.0.3 - '@octokit/auth-oauth-user': 6.0.2 - '@octokit/request': 10.0.7 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - '@octokit/auth-oauth-device@6.1.0': dependencies: '@octokit/oauth-methods': 4.1.0 @@ -19853,13 +19528,6 @@ snapshots: '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - '@octokit/auth-oauth-device@8.0.3': - dependencies: - '@octokit/oauth-methods': 6.0.2 - '@octokit/request': 10.0.7 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - '@octokit/auth-oauth-user@4.1.0': dependencies: '@octokit/auth-oauth-device': 6.1.0 @@ -19869,14 +19537,6 @@ snapshots: btoa-lite: 1.0.0 universal-user-agent: 6.0.1 - '@octokit/auth-oauth-user@6.0.2': - dependencies: - '@octokit/auth-oauth-device': 8.0.3 - '@octokit/oauth-methods': 6.0.2 - '@octokit/request': 10.0.7 - '@octokit/types': 16.0.0 - universal-user-agent: 7.0.3 - '@octokit/auth-token@4.0.0': {} '@octokit/auth-token@6.0.0': {} @@ -19930,8 +19590,6 @@ snapshots: '@octokit/oauth-authorization-url@6.0.2': {} - '@octokit/oauth-authorization-url@8.0.0': {} - '@octokit/oauth-methods@4.1.0': dependencies: '@octokit/oauth-authorization-url': 6.0.2 @@ -19940,13 +19598,6 @@ snapshots: '@octokit/types': 13.10.0 btoa-lite: 1.0.0 - '@octokit/oauth-methods@6.0.2': - dependencies: - '@octokit/oauth-authorization-url': 8.0.0 - '@octokit/request': 10.0.7 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - '@octokit/openapi-types@20.0.0': {} '@octokit/openapi-types@24.2.0': {} @@ -19970,11 +19621,6 @@ snapshots: '@octokit/core': 7.0.6 '@octokit/types': 15.0.2 - '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': - dependencies: - '@octokit/core': 7.0.6 - '@octokit/types': 16.0.0 - '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.6)': dependencies: '@octokit/core': 7.0.6 @@ -20039,13 +19685,6 @@ snapshots: '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) '@octokit/plugin-rest-endpoint-methods': 16.1.1(@octokit/core@7.0.6) - '@octokit/rest@22.0.1': - dependencies: - '@octokit/core': 7.0.6 - '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) - '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) - '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) - '@octokit/types@12.6.0': dependencies: '@octokit/openapi-types': 20.0.0 @@ -21015,32 +20654,6 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@posthog/ai@7.8.2(@ai-sdk/provider@3.0.8)(@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.3.6))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(posthog-node@5.24.7)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@4.3.6))': - dependencies: - '@anthropic-ai/sdk': 0.71.2(zod@4.3.6) - '@google/genai': 1.39.0(@modelcontextprotocol/sdk@1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.3.6)) - '@langchain/core': 1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6)) - '@posthog/core': 1.17.0 - langchain: 1.2.16(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6))(zod-to-json-schema@3.25.1(zod@4.3.6)) - openai: 6.17.0(ws@8.19.0)(zod@4.3.6) - posthog-node: 5.24.7 - uuid: 11.1.0 - zod: 4.3.6 - optionalDependencies: - '@ai-sdk/provider': 3.0.8 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - bufferutil - - react - - react-dom - - supports-color - - utf-8-validate - - ws - - zod-to-json-schema - '@posthog/core@1.17.0': dependencies: cross-spawn: 7.0.6 @@ -22140,10 +21753,6 @@ snapshots: '@restatedev/restate-linux-x64@1.6.0': optional: true - '@restatedev/restate-sdk-clients@1.10.2': - dependencies: - '@restatedev/restate-sdk-core': 1.10.2 - '@restatedev/restate-sdk-cloudflare-workers@1.10.2': dependencies: '@restatedev/restate-sdk-core': 1.10.2 @@ -22252,23 +21861,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true - '@scalar/core@0.1.1': - dependencies: - '@scalar/types': 0.0.40 - - '@scalar/hono-api-reference@0.5.184(hono@4.11.7)': - dependencies: - '@scalar/core': 0.1.1 - hono: 4.11.7 - - '@scalar/openapi-types@0.1.9': {} - - '@scalar/types@0.0.40': - dependencies: - '@scalar/openapi-types': 0.1.9 - '@unhead/schema': 1.11.20 - zod: 3.25.76 - '@scarf/scarf@1.4.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -24466,11 +24058,6 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@unhead/schema@1.11.20': - dependencies: - hookable: 5.5.3 - zhead: 2.2.4 - '@unpic/core@1.0.3': dependencies: unpic: 4.2.2 @@ -25883,8 +25470,6 @@ snapshots: clone@1.0.4: {} - clone@2.1.2: {} - clsx@1.2.1: {} clsx@2.1.1: {} @@ -28111,26 +27696,12 @@ snapshots: dependencies: react-is: 16.13.1 - hono-openapi@0.4.8(@hono/zod-validator@0.7.6(hono@4.11.7)(zod@4.3.6))(@sinclair/typebox@0.34.48)(effect@3.19.15)(hono@4.11.7)(openapi-types@12.1.3)(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6): - dependencies: - json-schema-walker: 2.0.0 - openapi-types: 12.1.3 - optionalDependencies: - '@hono/zod-validator': 0.7.6(hono@4.11.7)(zod@4.3.6) - '@sinclair/typebox': 0.34.48 - effect: 3.19.15 - hono: 4.11.7 - zod: 4.3.6 - zod-openapi: 5.4.6(zod@4.3.6) - hono-rate-limiter@0.4.2(hono@4.11.7): dependencies: hono: 4.11.7 hono@4.11.7: {} - hookable@5.5.3: {} - hookified@1.15.1: {} hosted-git-info@7.0.2: @@ -28784,22 +28355,12 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.28.6 - ts-algebra: 2.0.0 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} - json-schema-walker@2.0.0: - dependencies: - '@apidevtools/json-schema-ref-parser': 11.9.3 - clone: 2.1.2 - json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -28913,23 +28474,6 @@ snapshots: dotenv: 16.6.1 winston: 3.19.0 - langchain@1.2.16(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6))(zod-to-json-schema@3.25.1(zod@4.3.6)): - dependencies: - '@langchain/core': 1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6)) - '@langchain/langgraph': 1.1.2(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@4.3.6))(zod@4.3.6) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6))) - langsmith: 0.4.12(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.19.0)(zod@4.3.6)) - uuid: 10.0.0 - zod: 4.3.6 - transitivePeerDependencies: - - '@opentelemetry/api' - - '@opentelemetry/exporter-trace-otlp-proto' - - '@opentelemetry/sdk-trace-base' - - openai - - react - - react-dom - - zod-to-json-schema - langium@3.3.1: dependencies: chevrotain: 11.0.3 @@ -30783,8 +30327,6 @@ snapshots: dependencies: openapi-typescript-helpers: 0.0.15 - openapi-types@12.1.3: {} - openapi-typescript-helpers@0.0.15: {} optionator@0.9.4: @@ -31400,10 +30942,6 @@ snapshots: transitivePeerDependencies: - debug - posthog-node@5.24.7: - dependencies: - '@posthog/core': 1.17.0 - powershell-utils@0.1.0: {} preact@10.28.3: {} @@ -33416,8 +32954,6 @@ snapshots: trough@2.2.0: {} - ts-algebra@2.0.0: {} - ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -33691,8 +33227,6 @@ snapshots: '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - universal-github-app-jwt@2.2.2: {} - universal-user-agent@6.0.1: {} universal-user-agent@7.0.3: {} @@ -34627,18 +34161,12 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 - zhead@2.2.4: {} - zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-openapi@5.4.6(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76