Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,4 @@ jobs:
- name: Build CLI and Desktop
run: bun turbo run build --filter=@superset/cli --filter=@superset/desktop
env:
# TODO: Remove once desktop app no longer validates STREAMS_* at build time
STREAMS_URL: ${{ secrets.STREAMS_URL }}
STREAMS_SECRET: ${{ secrets.STREAMS_SECRET }}
14 changes: 13 additions & 1 deletion .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,21 @@ jobs:
deploy-streams-preview:
name: Deploy Streams (Fly.io)
runs-on: ubuntu-latest
needs: deploy-database

steps:
- uses: actions/checkout@v4

- name: Download database info
uses: actions/download-artifact@v4
with:
name: database-status

- name: Load database URL
run: |
source database-status.env
echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
Comment on lines +137 to +140
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Quote the variable to prevent word splitting.

Per the static analysis hint (SC2086), $DATABASE_URL should be double-quoted to prevent globbing and word splitting, especially since connection strings can contain special characters.

Proposed fix
       - name: Load database URL
         run: |
           source database-status.env
-          echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
+          echo "DATABASE_URL=${DATABASE_URL}" >> "$GITHUB_ENV"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Load database URL
run: |
source database-status.env
echo "DATABASE_URL=$DATABASE_URL" >> $GITHUB_ENV
- name: Load database URL
run: |
source database-status.env
echo "DATABASE_URL=${DATABASE_URL}" >> "$GITHUB_ENV"
🧰 Tools
🪛 actionlint (1.7.10)

[error] 138-138: shellcheck reported issue in this script: SC2086:info:2:38: Double quote to prevent globbing and word splitting

(shellcheck)

🤖 Prompt for AI Agents
In @.github/workflows/deploy-preview.yml around lines 137 - 140, The step named
"Load database URL" is leaving variable expansions unquoted; update the echo to
quote the DATABASE_URL expansion and the GITHUB_ENV target to prevent word
splitting/globbing: change the echo line so it uses the $DATABASE_URL variable
inside double quotes (e.g. echo "DATABASE_URL=$DATABASE_URL" with the expansion
quoted) and quote $GITHUB_ENV in the redirection (>> "$GITHUB_ENV"); reference
the step name "Load database URL" and the variables $DATABASE_URL and
$GITHUB_ENV when making the change.


- name: Deploy Streams preview to Fly.io
uses: superfly/fly-pr-review-apps@1.3.0
with:
Expand All @@ -135,7 +147,7 @@ jobs:
org: ${{ vars.FLY_ORG }}
config: apps/streams/fly.toml
secrets: |
STREAMS_SECRET=${{ secrets.STREAMS_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Save streams status
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ jobs:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
flyctl secrets set \
STREAMS_SECRET="${{ secrets.STREAMS_SECRET }}" \
DATABASE_URL="${{ secrets.DATABASE_URL }}" \
--app superset-stream \
--stage
- name: Deploy to Fly.io
Expand Down
20 changes: 1 addition & 19 deletions .superset/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,6 @@ step_start_electric() {
return 0
}

step_setup_streams() {
echo "🔄 Setting up Streams secret..."

STREAMS_SECRET=$(openssl rand -hex 32)
export STREAMS_SECRET

success "Streams secret generated"
return 0
}

step_write_env() {
echo "📝 Writing .env file..."

Expand Down Expand Up @@ -352,9 +342,6 @@ step_write_env() {
echo "ELECTRIC_SECRET=$ELECTRIC_SECRET"
fi

echo ""
echo "# Workspace Streams (AI Chat Server)"
echo "STREAMS_SECRET=$STREAMS_SECRET"
} >> .env

success "Workspace .env written"
Expand Down Expand Up @@ -390,12 +377,7 @@ main() {
step_failed "Start Electric SQL"
fi

# Step 6: Setup Streams secret
if ! step_setup_streams; then
step_failed "Setup Streams secret"
fi

# Step 7: Write .env file
# Step 6: Write .env file
if ! step_write_env; then
step_failed "Write .env file"
fi
Expand Down
12 changes: 8 additions & 4 deletions apps/desktop/src/lib/trpc/routers/ai-chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from "node:path";
import { env } from "main/env.main";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import { loadToken } from "../auth/utils/auth-functions";
import {
readClaudeSessionMessages,
scanClaudeSessions,
Expand Down Expand Up @@ -55,10 +56,13 @@ function scanCustomCommands(cwd: string): CommandEntry[] {

export const createAiChatRouter = () => {
return router({
getConfig: publicProcedure.query(() => ({
proxyUrl: env.STREAMS_URL,
authToken: env.STREAMS_SECRET,
})),
getConfig: publicProcedure.query(async () => {
const { token } = await loadToken();
return {
proxyUrl: env.STREAMS_URL,
authToken: token,
};
}),

getSlashCommands: publicProcedure
.input(z.object({ cwd: z.string() }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
} from "@superset/agent";
import { app } from "electron";
import { env } from "main/env.main";
import { loadToken } from "../../../auth/utils/auth-functions";
import { buildClaudeEnv } from "../auth";
import type { SessionStore } from "../session-store";

const PROXY_URL = env.STREAMS_URL;
const STREAMS_SECRET = env.STREAMS_SECRET;

function getClaudeBinaryPath(): string {
if (app.isPackaged) {
Expand All @@ -30,10 +30,14 @@ function getClaudeBinaryPath(): string {
);
}

function buildProxyHeaders(): Record<string, string> {
async function buildProxyHeaders(): Promise<Record<string, string>> {
const { token } = await loadToken();
if (!token) {
throw new Error("User not authenticated");
}
return {
"Content-Type": "application/json",
Authorization: `Bearer ${STREAMS_SECRET}`,
Authorization: `Bearer ${token}`,
};
}

Expand Down Expand Up @@ -97,7 +101,7 @@ export class ChatSessionManager extends EventEmitter {
permissionMode?: string;
maxThinkingTokens?: number;
}): Promise<void> {
const headers = buildProxyHeaders();
const headers = await buildProxyHeaders();

const createRes = await fetch(`${PROXY_URL}/v1/sessions/${sessionId}`, {
method: "PUT",
Expand Down Expand Up @@ -258,7 +262,7 @@ export class ChatSessionManager extends EventEmitter {
const abortController = new AbortController();
this.runningAgents.set(sessionId, abortController);

const headers = buildProxyHeaders();
const headers = await buildProxyHeaders();
let messageId: string | undefined;

try {
Expand Down Expand Up @@ -455,7 +459,7 @@ export class ChatSessionManager extends EventEmitter {
try {
await fetch(`${PROXY_URL}/v1/sessions/${sessionId}/stop`, {
method: "POST",
headers: buildProxyHeaders(),
headers: await buildProxyHeaders(),
body: JSON.stringify({}),
});
} catch (error) {
Expand All @@ -479,7 +483,7 @@ export class ChatSessionManager extends EventEmitter {
try {
await fetch(`${PROXY_URL}/v1/sessions/${sessionId}/stop`, {
method: "POST",
headers: buildProxyHeaders(),
headers: await buildProxyHeaders(),
body: JSON.stringify({}),
});
} catch (err) {
Expand Down Expand Up @@ -516,7 +520,7 @@ export class ChatSessionManager extends EventEmitter {

async deleteSession({ sessionId }: { sessionId: string }): Promise<void> {
console.log(`[chat/session] Deleting session ${sessionId}`);
const headers = buildProxyHeaders();
const headers = await buildProxyHeaders();

const controller = this.runningAgents.get(sessionId);
if (controller) {
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/main/env.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const env = createEnv({
NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"),
SENTRY_DSN_DESKTOP: z.string().optional(),
STREAMS_URL: z.url(),
STREAMS_SECRET: z.string().min(1),
},

runtimeEnv: {
Expand All @@ -34,7 +33,6 @@ export const env = createEnv({
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
SENTRY_DSN_DESKTOP: process.env.SENTRY_DSN_DESKTOP,
STREAMS_URL: process.env.STREAMS_URL,
STREAMS_SECRET: process.env.STREAMS_SECRET,
},
emptyStringAsUndefined: true,
// Only allow skipping validation in development (never in production)
Expand Down
6 changes: 5 additions & 1 deletion apps/streams/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ WORKDIR /app
# Install dependencies (workspace root)
COPY package.json bun.lock ./
COPY tooling/typescript/package.json tooling/typescript/
COPY packages/db/package.json packages/db/
COPY apps/streams/package.json apps/streams/
RUN bun install --frozen-lockfile

# Copy source
COPY tooling/typescript tooling/typescript
COPY packages/db packages/db
COPY apps/streams apps/streams

# Build
Expand All @@ -28,10 +30,12 @@ ENV NODE_ENV=production
# Install production dependencies (workspace root)
COPY package.json bun.lock ./
COPY tooling/typescript/package.json tooling/typescript/
COPY packages/db/package.json packages/db/
COPY apps/streams/package.json apps/streams/
RUN bun install --frozen-lockfile --production

# Copy built files
# Copy built files and db package (needed at runtime for schema)
COPY --from=builder /app/packages/db ./packages/db
COPY --from=builder /app/apps/streams/dist ./apps/streams/dist

WORKDIR /app/apps/streams
Expand Down
2 changes: 2 additions & 0 deletions apps/streams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
"@durable-streams/client": "^0.2.1",
"@durable-streams/server": "^0.2.0",
"@hono/node-server": "^1.13.0",
"@superset/db": "workspace:*",
"@superset/durable-session": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@tanstack/ai": "^0.3.0",
"@tanstack/db": "0.5.25",
"drizzle-orm": "0.45.1",
"hono": "^4.4.0",
"zod": "^4.3.5"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/streams/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const env = createEnv({
STREAMS_INTERNAL_PORT: z.coerce.number(),
STREAMS_INTERNAL_URL: z.string().url(),
STREAMS_DATA_DIR: z.string().min(1),
STREAMS_SECRET: z.string().min(1),
DATABASE_URL: z.string().url(),
},
clientPrefix: "PUBLIC_",
client: {},
Expand Down
1 change: 0 additions & 1 deletion apps/streams/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const { app } = createServer({
baseUrl: env.STREAMS_INTERNAL_URL,
cors: true,
logging: true,
authToken: env.STREAMS_SECRET,
});

const proxyServer = serve(
Expand Down
42 changes: 30 additions & 12 deletions apps/streams/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { db } from "@superset/db";
import { sessions } from "@superset/db/schema/auth";
import { and, eq, gt } from "drizzle-orm";
Comment on lines +1 to +3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate packages/db directory structure
cd apps/streams || cd . 2>/dev/null
find . -path "*/packages/db*" -type d 2>/dev/null | head -5

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for database client initialization in packages/db
fd -t f '\.(ts|js)$' packages/db --exec head -50 {} \; 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 2334


🏁 Script executed:

#!/bin/bash
# Look for DATABASE_URL references in packages/db
rg 'DATABASE_URL' packages/db 2>/dev/null || echo "No matches found"

Repository: superset-sh/superset

Length of output: 415


🏁 Script executed:

#!/bin/bash
# Check the structure of packages/db
ls -la packages/db/ 2>/dev/null || echo "Directory not found at packages/db"

Repository: superset-sh/superset

Length of output: 484


🏁 Script executed:

#!/bin/bash
# Get the full content of env.ts in packages/db
cat packages/db/src/env.ts

Repository: superset-sh/superset

Length of output: 554


🏁 Script executed:

#!/bin/bash
# Check if there's any dotenv config or initialization in packages/db
rg 'config|dotenv' packages/db/src/ -A 2 -B 2

Repository: superset-sh/superset

Length of output: 1423


Remove redundant dotenv config from client.ts; DATABASE_URL resolution is already handled correctly by env.ts.

The db singleton correctly reads DATABASE_URL from the streams server's environment because packages/db/src/env.ts uses an absolute path to load .env from the monorepo root: config({ path: path.resolve(__dirname, "../../../.env"), quiet: true }). However, packages/db/src/client.ts has a redundant config({ path: ".env", quiet: true }) with a relative path that is unnecessary and creates confusion. Remove this line since env.ts is imported immediately after and handles the environment setup correctly.

🤖 Prompt for AI Agents
In `@apps/streams/src/server.ts` around lines 1 - 3, Remove the redundant dotenv
initialization in client.ts: delete the config({ path: ".env", quiet: true })
call in packages/db/src/client.ts (the one before importing env.ts), since
env.ts already calls config(...) with the absolute monorepo path; keep the
import of env.ts and the db singleton logic intact so DATABASE_URL resolution
continues to come from env.ts.

import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
Expand All @@ -16,15 +19,20 @@ import {
} from "./routes";
import type { AIDBProtocolOptions } from "./types";

type SessionEnv = {
Variables: {
userId: string;
};
};

export interface AIDBProxyServerOptions extends AIDBProtocolOptions {
cors?: boolean;
logging?: boolean;
corsOrigins?: string | string[];
authToken?: string;
}

export function createServer(options: AIDBProxyServerOptions) {
const app = new Hono();
const app = new Hono<SessionEnv>();

const protocol = new AIDBSessionProtocol({
baseUrl: options.baseUrl,
Expand Down Expand Up @@ -56,16 +64,26 @@ export function createServer(options: AIDBProxyServerOptions) {
app.route("/health", createHealthRoutes());

// No auth on health; Bearer token required on /v1/*
if (options.authToken) {
const expectedHeader = `Bearer ${options.authToken}`;
app.use("/v1/*", async (c, next) => {
const authorization = c.req.header("Authorization");
if (authorization !== expectedHeader) {
return c.json({ error: "Unauthorized" }, 401);
}
return next();
});
}
app.use("/v1/*", async (c, next) => {
const authorization = c.req.header("Authorization");
if (!authorization?.startsWith("Bearer ")) {
return c.json({ error: "Unauthorized" }, 401);
}

const token = authorization.slice(7);
const [session] = await db
.select({ userId: sessions.userId })
.from(sessions)
.where(and(eq(sessions.token, token), gt(sessions.expiresAt, new Date())))
.limit(1);

if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}

c.set("userId", session.userId);
return next();
});

const v1 = new Hono();
v1.route("/sessions", createSessionRoutes(protocol));
Expand Down
7 changes: 3 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading