diff --git a/.env.development.example b/.env.development.example index 24bbef1..c4dac97 100644 --- a/.env.development.example +++ b/.env.development.example @@ -1,10 +1,14 @@ -# Load this file only when NODE_ENV=development. -# The backend does not load dotenv files in production. +# Foundry local development environment. +# Copy ~/misc/the-foundry.env to .env in the repo root to populate secrets. +# .env is gitignored — never commit it. The source of truth is ~/misc/the-foundry.env. +# +# Docker Compose (just foundry-dev) and the justfile (set dotenv-load := true) +# both read .env automatically. APP_URL=http://localhost:4173 BETTER_AUTH_URL=http://localhost:4173 BETTER_AUTH_SECRET=sandbox-agent-foundry-development-only-change-me -GITHUB_REDIRECT_URI=http://localhost:4173/api/rivet/app/auth/github/callback +GITHUB_REDIRECT_URI=http://localhost:4173/v1/auth/github/callback # Fill these in when enabling live GitHub OAuth. GITHUB_CLIENT_ID= diff --git a/foundry/CLAUDE.md b/foundry/CLAUDE.md index 074514f..2e6e82e 100644 --- a/foundry/CLAUDE.md +++ b/foundry/CLAUDE.md @@ -126,7 +126,7 @@ For all Rivet/RivetKit implementation: - Request/action contract: wait only until the minimum resource needed for the client's next step exists. Example: task creation may wait for task actor creation/identity, but not for sandbox provisioning or session bootstrap. - Read paths must not force refresh/sync work inline. Serve the latest cached projection, mark staleness explicitly, and trigger background refresh separately when needed. - If a workflow needs to resume after some external work completes, model that as workflow state plus follow-up messages/events instead of holding the original request open. -- Do not rely on retries for correctness or normal control flow. If a queue/workflow/external dependency is not ready yet, model that explicitly and resume from a push/event, instead of polling or retry loops. +- No retries: never add retry loops (`withRetries`, `setTimeout` retry, exponential backoff) anywhere in the codebase. If an operation fails, surface the error immediately. If a dependency is not ready yet, model that explicitly with workflow state and resume from a push/event instead of polling or retry loops. - Actor handle policy: - Prefer explicit `get` or explicit `create` based on workflow intent; do not default to `getOrCreate`. - Use `get`/`getForId` when the actor is expected to already exist; if missing, surface an explicit `Actor not found` error with recovery context. diff --git a/foundry/compose.dev.yaml b/foundry/compose.dev.yaml index 01c6934..657ebbe 100644 --- a/foundry/compose.dev.yaml +++ b/foundry/compose.dev.yaml @@ -41,6 +41,7 @@ services: HF_DAYTONA_ENDPOINT: "${HF_DAYTONA_ENDPOINT:-}" HF_DAYTONA_API_KEY: "${HF_DAYTONA_API_KEY:-}" ports: + - "6420:6420" - "7741:7741" volumes: - "..:/app" diff --git a/foundry/packages/backend/package.json b/foundry/packages/backend/package.json index 4f65032..aec80a0 100644 --- a/foundry/packages/backend/package.json +++ b/foundry/packages/backend/package.json @@ -19,6 +19,7 @@ "@iarna/toml": "^2.2.5", "@sandbox-agent/foundry-shared": "workspace:*", "@sandbox-agent/persist-rivet": "workspace:*", + "better-auth": "^1.5.5", "drizzle-kit": "^0.31.8", "drizzle-orm": "^0.44.5", "hono": "^4.11.9", diff --git a/foundry/packages/backend/src/actors/auth-user/db/db.ts b/foundry/packages/backend/src/actors/auth-user/db/db.ts new file mode 100644 index 0000000..b434338 --- /dev/null +++ b/foundry/packages/backend/src/actors/auth-user/db/db.ts @@ -0,0 +1,5 @@ +import { db } from "rivetkit/db/drizzle"; +import * as schema from "./schema.js"; +import migrations from "./migrations.js"; + +export const authUserDb = db({ schema, migrations }); diff --git a/foundry/packages/backend/src/actors/auth-user/db/migrations.ts b/foundry/packages/backend/src/actors/auth-user/db/migrations.ts new file mode 100644 index 0000000..be7cb17 --- /dev/null +++ b/foundry/packages/backend/src/actors/auth-user/db/migrations.ts @@ -0,0 +1,80 @@ +// This file is generated by src/actors/_scripts/generate-actor-migrations.ts. +// Source of truth is drizzle-kit output under ./drizzle (meta/_journal.json + *.sql). +// Do not hand-edit this file. + +const journal = { + entries: [ + { + idx: 0, + when: 1773446400000, + tag: "0000_auth_user", + breakpoints: true, + }, + ], +} as const; + +export default { + journal, + migrations: { + m0000: `CREATE TABLE \`user\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`name\` text NOT NULL, + \`email\` text NOT NULL, + \`email_verified\` integer NOT NULL, + \`image\` text, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`token\` text NOT NULL, + \`user_id\` text NOT NULL, + \`expires_at\` integer NOT NULL, + \`ip_address\` text, + \`user_agent\` text, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX \`session_token_idx\` ON \`session\` (\`token\`); +--> statement-breakpoint +CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`account_id\` text NOT NULL, + \`provider_id\` text NOT NULL, + \`user_id\` text NOT NULL, + \`access_token\` text, + \`refresh_token\` text, + \`id_token\` text, + \`access_token_expires_at\` integer, + \`refresh_token_expires_at\` integer, + \`scope\` text, + \`password\` text, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX \`account_provider_account_idx\` ON \`account\` (\`provider_id\`, \`account_id\`); +--> statement-breakpoint +CREATE TABLE \`user_profiles\` ( + \`user_id\` text PRIMARY KEY NOT NULL, + \`github_account_id\` text, + \`github_login\` text, + \`role_label\` text NOT NULL, + \`eligible_organization_ids_json\` text NOT NULL, + \`starter_repo_status\` text NOT NULL, + \`starter_repo_starred_at\` integer, + \`starter_repo_skipped_at\` integer, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE \`session_state\` ( + \`session_id\` text PRIMARY KEY NOT NULL, + \`active_organization_id\` text, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +);`, + } as const, +}; diff --git a/foundry/packages/backend/src/actors/auth-user/db/schema.ts b/foundry/packages/backend/src/actors/auth-user/db/schema.ts new file mode 100644 index 0000000..b87567a --- /dev/null +++ b/foundry/packages/backend/src/actors/auth-user/db/schema.ts @@ -0,0 +1,70 @@ +import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; + +export const authUsers = sqliteTable("user", { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + email: text("email").notNull(), + emailVerified: integer("email_verified").notNull(), + image: text("image"), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), +}); + +export const authSessions = sqliteTable( + "session", + { + id: text("id").notNull().primaryKey(), + token: text("token").notNull(), + userId: text("user_id").notNull(), + expiresAt: integer("expires_at").notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), + }, + (table) => ({ + tokenIdx: uniqueIndex("session_token_idx").on(table.token), + }), +); + +export const authAccounts = sqliteTable( + "account", + { + id: text("id").notNull().primaryKey(), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id").notNull(), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: integer("access_token_expires_at"), + refreshTokenExpiresAt: integer("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), + }, + (table) => ({ + providerAccountIdx: uniqueIndex("account_provider_account_idx").on(table.providerId, table.accountId), + }), +); + +export const userProfiles = sqliteTable("user_profiles", { + userId: text("user_id").notNull().primaryKey(), + githubAccountId: text("github_account_id"), + githubLogin: text("github_login"), + roleLabel: text("role_label").notNull(), + eligibleOrganizationIdsJson: text("eligible_organization_ids_json").notNull(), + starterRepoStatus: text("starter_repo_status").notNull(), + starterRepoStarredAt: integer("starter_repo_starred_at"), + starterRepoSkippedAt: integer("starter_repo_skipped_at"), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), +}); + +export const sessionState = sqliteTable("session_state", { + sessionId: text("session_id").notNull().primaryKey(), + activeOrganizationId: text("active_organization_id"), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), +}); diff --git a/foundry/packages/backend/src/actors/auth-user/index.ts b/foundry/packages/backend/src/actors/auth-user/index.ts new file mode 100644 index 0000000..a77635a --- /dev/null +++ b/foundry/packages/backend/src/actors/auth-user/index.ts @@ -0,0 +1,353 @@ +import { and, asc, count as sqlCount, desc, eq, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, notInArray, or } from "drizzle-orm"; +import { actor } from "rivetkit"; +import { authUserDb } from "./db/db.js"; +import { authAccounts, authSessions, authUsers, sessionState, userProfiles } from "./db/schema.js"; + +const tables = { + user: authUsers, + session: authSessions, + account: authAccounts, + userProfiles, + sessionState, +} as const; + +function tableFor(model: string) { + const table = tables[model as keyof typeof tables]; + if (!table) { + throw new Error(`Unsupported auth user model: ${model}`); + } + return table as any; +} + +function columnFor(table: any, field: string) { + const column = table[field]; + if (!column) { + throw new Error(`Unsupported auth user field: ${field}`); + } + return column; +} + +function normalizeValue(value: unknown): unknown { + if (value instanceof Date) { + return value.getTime(); + } + if (Array.isArray(value)) { + return value.map((entry) => normalizeValue(entry)); + } + return value; +} + +function clauseToExpr(table: any, clause: any) { + const column = columnFor(table, clause.field); + const value = normalizeValue(clause.value); + + switch (clause.operator) { + case "ne": + return value === null ? isNotNull(column) : ne(column, value as any); + case "lt": + return lt(column, value as any); + case "lte": + return lte(column, value as any); + case "gt": + return gt(column, value as any); + case "gte": + return gte(column, value as any); + case "in": + return inArray(column, Array.isArray(value) ? (value as any[]) : [value as any]); + case "not_in": + return notInArray(column, Array.isArray(value) ? (value as any[]) : [value as any]); + case "contains": + return like(column, `%${String(value ?? "")}%`); + case "starts_with": + return like(column, `${String(value ?? "")}%`); + case "ends_with": + return like(column, `%${String(value ?? "")}`); + case "eq": + default: + return value === null ? isNull(column) : eq(column, value as any); + } +} + +function buildWhere(table: any, where: any[] | undefined) { + if (!where || where.length === 0) { + return undefined; + } + + let expr = clauseToExpr(table, where[0]); + for (const clause of where.slice(1)) { + const next = clauseToExpr(table, clause); + expr = clause.connector === "OR" ? or(expr, next) : and(expr, next); + } + return expr; +} + +function applyJoinToRow(c: any, model: string, row: any, join: any) { + if (!row || !join) { + return row; + } + + if (model === "session" && join.user) { + return c.db + .select() + .from(authUsers) + .where(eq(authUsers.id, row.userId)) + .get() + .then((user: any) => ({ ...row, user: user ?? null })); + } + + if (model === "account" && join.user) { + return c.db + .select() + .from(authUsers) + .where(eq(authUsers.id, row.userId)) + .get() + .then((user: any) => ({ ...row, user: user ?? null })); + } + + if (model === "user" && join.account) { + return c.db + .select() + .from(authAccounts) + .where(eq(authAccounts.userId, row.id)) + .all() + .then((accounts: any[]) => ({ ...row, account: accounts })); + } + + return Promise.resolve(row); +} + +async function applyJoinToRows(c: any, model: string, rows: any[], join: any) { + if (!join || rows.length === 0) { + return rows; + } + + if (model === "session" && join.user) { + const userIds = [...new Set(rows.map((row) => row.userId).filter(Boolean))]; + const users = userIds.length > 0 ? await c.db.select().from(authUsers).where(inArray(authUsers.id, userIds)).all() : []; + const userMap = new Map(users.map((user: any) => [user.id, user])); + return rows.map((row) => ({ ...row, user: userMap.get(row.userId) ?? null })); + } + + if (model === "account" && join.user) { + const userIds = [...new Set(rows.map((row) => row.userId).filter(Boolean))]; + const users = userIds.length > 0 ? await c.db.select().from(authUsers).where(inArray(authUsers.id, userIds)).all() : []; + const userMap = new Map(users.map((user: any) => [user.id, user])); + return rows.map((row) => ({ ...row, user: userMap.get(row.userId) ?? null })); + } + + if (model === "user" && join.account) { + const userIds = rows.map((row) => row.id); + const accounts = userIds.length > 0 ? await c.db.select().from(authAccounts).where(inArray(authAccounts.userId, userIds)).all() : []; + const accountsByUserId = new Map(); + for (const account of accounts) { + const entries = accountsByUserId.get(account.userId) ?? []; + entries.push(account); + accountsByUserId.set(account.userId, entries); + } + return rows.map((row) => ({ ...row, account: accountsByUserId.get(row.id) ?? [] })); + } + + return rows; +} + +export const authUser = actor({ + db: authUserDb, + options: { + name: "Auth User", + icon: "shield", + actionTimeout: 60_000, + }, + createState: (_c, input: { userId: string }) => ({ + userId: input.userId, + }), + actions: { + async createAuthRecord(c, input: { model: string; data: Record }) { + const table = tableFor(input.model); + await c.db + .insert(table) + .values(input.data as any) + .run(); + return await c.db + .select() + .from(table) + .where(eq(columnFor(table, "id"), input.data.id as any)) + .get(); + }, + + async findOneAuthRecord(c, input: { model: string; where: any[]; join?: any }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + const row = predicate ? await c.db.select().from(table).where(predicate).get() : await c.db.select().from(table).get(); + return await applyJoinToRow(c, input.model, row ?? null, input.join); + }, + + async findManyAuthRecords(c, input: { model: string; where?: any[]; limit?: number; offset?: number; sortBy?: any; join?: any }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + let query: any = c.db.select().from(table); + if (predicate) { + query = query.where(predicate); + } + if (input.sortBy?.field) { + const column = columnFor(table, input.sortBy.field); + query = query.orderBy(input.sortBy.direction === "asc" ? asc(column) : desc(column)); + } + if (typeof input.limit === "number") { + query = query.limit(input.limit); + } + if (typeof input.offset === "number") { + query = query.offset(input.offset); + } + const rows = await query.all(); + return await applyJoinToRows(c, input.model, rows, input.join); + }, + + async updateAuthRecord(c, input: { model: string; where: any[]; update: Record }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + if (!predicate) { + throw new Error("updateAuthRecord requires a where clause"); + } + await c.db + .update(table) + .set(input.update as any) + .where(predicate) + .run(); + return await c.db.select().from(table).where(predicate).get(); + }, + + async updateManyAuthRecords(c, input: { model: string; where: any[]; update: Record }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + if (!predicate) { + throw new Error("updateManyAuthRecords requires a where clause"); + } + await c.db + .update(table) + .set(input.update as any) + .where(predicate) + .run(); + const row = await c.db.select({ value: sqlCount() }).from(table).where(predicate).get(); + return row?.value ?? 0; + }, + + async deleteAuthRecord(c, input: { model: string; where: any[] }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + if (!predicate) { + throw new Error("deleteAuthRecord requires a where clause"); + } + await c.db.delete(table).where(predicate).run(); + }, + + async deleteManyAuthRecords(c, input: { model: string; where: any[] }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + if (!predicate) { + throw new Error("deleteManyAuthRecords requires a where clause"); + } + const rows = await c.db.select().from(table).where(predicate).all(); + await c.db.delete(table).where(predicate).run(); + return rows.length; + }, + + async countAuthRecords(c, input: { model: string; where?: any[] }) { + const table = tableFor(input.model); + const predicate = buildWhere(table, input.where); + const row = predicate + ? await c.db.select({ value: sqlCount() }).from(table).where(predicate).get() + : await c.db.select({ value: sqlCount() }).from(table).get(); + return row?.value ?? 0; + }, + + async getAppAuthState(c, input: { sessionId: string }) { + const session = await c.db.select().from(authSessions).where(eq(authSessions.id, input.sessionId)).get(); + if (!session) { + return null; + } + const [user, profile, currentSessionState, accounts] = await Promise.all([ + c.db.select().from(authUsers).where(eq(authUsers.id, session.userId)).get(), + c.db.select().from(userProfiles).where(eq(userProfiles.userId, session.userId)).get(), + c.db.select().from(sessionState).where(eq(sessionState.sessionId, input.sessionId)).get(), + c.db.select().from(authAccounts).where(eq(authAccounts.userId, session.userId)).all(), + ]); + return { + session, + user, + profile: profile ?? null, + sessionState: currentSessionState ?? null, + accounts, + }; + }, + + async upsertUserProfile( + c, + input: { + userId: string; + patch: { + githubAccountId?: string | null; + githubLogin?: string | null; + roleLabel?: string; + eligibleOrganizationIdsJson?: string; + starterRepoStatus?: string; + starterRepoStarredAt?: number | null; + starterRepoSkippedAt?: number | null; + }; + }, + ) { + const now = Date.now(); + await c.db + .insert(userProfiles) + .values({ + userId: input.userId, + githubAccountId: input.patch.githubAccountId ?? null, + githubLogin: input.patch.githubLogin ?? null, + roleLabel: input.patch.roleLabel ?? "GitHub user", + eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson ?? "[]", + starterRepoStatus: input.patch.starterRepoStatus ?? "pending", + starterRepoStarredAt: input.patch.starterRepoStarredAt ?? null, + starterRepoSkippedAt: input.patch.starterRepoSkippedAt ?? null, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: userProfiles.userId, + set: { + ...(input.patch.githubAccountId !== undefined ? { githubAccountId: input.patch.githubAccountId } : {}), + ...(input.patch.githubLogin !== undefined ? { githubLogin: input.patch.githubLogin } : {}), + ...(input.patch.roleLabel !== undefined ? { roleLabel: input.patch.roleLabel } : {}), + ...(input.patch.eligibleOrganizationIdsJson !== undefined ? { eligibleOrganizationIdsJson: input.patch.eligibleOrganizationIdsJson } : {}), + ...(input.patch.starterRepoStatus !== undefined ? { starterRepoStatus: input.patch.starterRepoStatus } : {}), + ...(input.patch.starterRepoStarredAt !== undefined ? { starterRepoStarredAt: input.patch.starterRepoStarredAt } : {}), + ...(input.patch.starterRepoSkippedAt !== undefined ? { starterRepoSkippedAt: input.patch.starterRepoSkippedAt } : {}), + updatedAt: now, + }, + }) + .run(); + + return await c.db.select().from(userProfiles).where(eq(userProfiles.userId, input.userId)).get(); + }, + + async upsertSessionState(c, input: { sessionId: string; activeOrganizationId: string | null }) { + const now = Date.now(); + await c.db + .insert(sessionState) + .values({ + sessionId: input.sessionId, + activeOrganizationId: input.activeOrganizationId, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: sessionState.sessionId, + set: { + activeOrganizationId: input.activeOrganizationId, + updatedAt: now, + }, + }) + .run(); + + return await c.db.select().from(sessionState).where(eq(sessionState.sessionId, input.sessionId)).get(); + }, + }, +}); diff --git a/foundry/packages/backend/src/actors/handles.ts b/foundry/packages/backend/src/actors/handles.ts index 228ce8c..02de614 100644 --- a/foundry/packages/backend/src/actors/handles.ts +++ b/foundry/packages/backend/src/actors/handles.ts @@ -1,4 +1,14 @@ -import { taskKey, taskStatusSyncKey, historyKey, projectBranchSyncKey, projectKey, projectPrSyncKey, sandboxInstanceKey, workspaceKey } from "./keys.js"; +import { + authUserKey, + taskKey, + taskStatusSyncKey, + historyKey, + projectBranchSyncKey, + projectKey, + projectPrSyncKey, + sandboxInstanceKey, + workspaceKey, +} from "./keys.js"; import type { ProviderId } from "@sandbox-agent/foundry-shared"; export function actorClient(c: any) { @@ -11,6 +21,16 @@ export async function getOrCreateWorkspace(c: any, workspaceId: string) { }); } +export async function getOrCreateAuthUser(c: any, userId: string) { + return await actorClient(c).authUser.getOrCreate(authUserKey(userId), { + createWithInput: { userId }, + }); +} + +export function getAuthUser(c: any, userId: string) { + return actorClient(c).authUser.get(authUserKey(userId)); +} + export async function getOrCreateProject(c: any, workspaceId: string, repoId: string, remoteUrl: string) { return await actorClient(c).project.getOrCreate(projectKey(workspaceId, repoId), { createWithInput: { @@ -125,3 +145,7 @@ export function selfProject(c: any) { export function selfSandboxInstance(c: any) { return actorClient(c).sandboxInstance.getForId(c.actorId); } + +export function selfAuthUser(c: any) { + return actorClient(c).authUser.getForId(c.actorId); +} diff --git a/foundry/packages/backend/src/actors/index.ts b/foundry/packages/backend/src/actors/index.ts index ca0f9b4..245b6a4 100644 --- a/foundry/packages/backend/src/actors/index.ts +++ b/foundry/packages/backend/src/actors/index.ts @@ -1,3 +1,4 @@ +import { authUser } from "./auth-user/index.js"; import { setup } from "rivetkit"; import { taskStatusSync } from "./task-status-sync/index.js"; import { task } from "./task/index.js"; @@ -22,6 +23,7 @@ export const registry = setup({ baseLogger: logger, }, use: { + authUser, workspace, project, task, @@ -35,6 +37,7 @@ export const registry = setup({ export * from "./context.js"; export * from "./events.js"; +export * from "./auth-user/index.js"; export * from "./task-status-sync/index.js"; export * from "./task/index.js"; export * from "./history/index.js"; diff --git a/foundry/packages/backend/src/actors/keys.ts b/foundry/packages/backend/src/actors/keys.ts index f6b210e..bec675f 100644 --- a/foundry/packages/backend/src/actors/keys.ts +++ b/foundry/packages/backend/src/actors/keys.ts @@ -4,6 +4,10 @@ export function workspaceKey(workspaceId: string): ActorKey { return ["ws", workspaceId]; } +export function authUserKey(userId: string): ActorKey { + return ["ws", "app", "user", userId]; +} + export function projectKey(workspaceId: string, repoId: string): ActorKey { return ["ws", workspaceId, "project", repoId]; } diff --git a/foundry/packages/backend/src/actors/project/actions.ts b/foundry/packages/backend/src/actors/project/actions.ts index 5ae47e6..bcd8f36 100644 --- a/foundry/packages/backend/src/actors/project/actions.ts +++ b/foundry/packages/backend/src/actors/project/actions.ts @@ -10,7 +10,7 @@ import { foundryRepoClonePath } from "../../services/foundry-paths.js"; import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js"; import { expectQueueResponse } from "../../services/queue.js"; import { withRepoGitLock } from "../../services/repo-git-lock.js"; -import { branches, taskIndex, prCache, repoMeta } from "./db/schema.js"; +import { branches, taskIndex, prCache, repoActionJobs, repoMeta } from "./db/schema.js"; import { deriveFallbackTitle } from "../../services/create-flow.js"; import { normalizeBaseBranchName } from "../../integrations/git-spice/index.js"; import { sortBranchesForOverview } from "./stack-model.js"; @@ -87,6 +87,7 @@ interface BranchSyncResult { interface RepoOverviewCommand {} interface RunRepoStackActionCommand { + jobId?: string; action: RepoStackAction; branchName?: string; parentBranch?: string; @@ -133,6 +134,90 @@ async function ensureProjectSyncActors(c: any, localPath: string): Promise c.state.syncActorsStarted = true; } +async function ensureRepoActionJobsTable(c: any): Promise { + await c.db.execute(` + CREATE TABLE IF NOT EXISTS repo_action_jobs ( + job_id text PRIMARY KEY NOT NULL, + action text NOT NULL, + branch_name text, + parent_branch text, + status text NOT NULL, + message text NOT NULL, + created_at integer NOT NULL, + updated_at integer NOT NULL, + completed_at integer + ) + `); +} + +async function writeRepoActionJob( + c: any, + input: { + jobId: string; + action: RepoStackAction; + branchName: string | null; + parentBranch: string | null; + status: "queued" | "running" | "completed" | "error"; + message: string; + createdAt?: number; + completedAt?: number | null; + }, +): Promise { + await ensureRepoActionJobsTable(c); + const now = Date.now(); + await c.db + .insert(repoActionJobs) + .values({ + jobId: input.jobId, + action: input.action, + branchName: input.branchName, + parentBranch: input.parentBranch, + status: input.status, + message: input.message, + createdAt: input.createdAt ?? now, + updatedAt: now, + completedAt: input.completedAt ?? null, + }) + .onConflictDoUpdate({ + target: repoActionJobs.jobId, + set: { + status: input.status, + message: input.message, + updatedAt: now, + completedAt: input.completedAt ?? null, + }, + }) + .run(); +} + +async function listRepoActionJobRows(c: any): Promise< + Array<{ + jobId: string; + action: RepoStackAction; + branchName: string | null; + parentBranch: string | null; + status: "queued" | "running" | "completed" | "error"; + message: string; + createdAt: number; + updatedAt: number; + completedAt: number | null; + }> +> { + await ensureRepoActionJobsTable(c); + const rows = await c.db.select().from(repoActionJobs).orderBy(desc(repoActionJobs.updatedAt)).limit(20).all(); + return rows.map((row: any) => ({ + jobId: row.jobId, + action: row.action, + branchName: row.branchName ?? null, + parentBranch: row.parentBranch ?? null, + status: row.status, + message: row.message, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + completedAt: row.completedAt ?? null, + })); +} + async function deleteStaleTaskIndexRow(c: any, taskId: string): Promise { try { await c.db.delete(taskIndex).where(eq(taskIndex.taskId, taskId)).run(); @@ -359,8 +444,6 @@ async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise false))) { + await writeRepoActionJob(c, { + jobId, + action, + branchName, + parentBranch, + status: "error", + message: "git-spice is not available for this repo", + createdAt: at, + completedAt: Date.now(), + }); return { + jobId, action, executed: false, + status: "error", message: "git-spice is not available for this repo", at, }; @@ -615,48 +721,77 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand } } - await withRepoGitLock(localPath, async () => { - if (action === "sync_repo") { - await driver.stack.syncRepo(localPath); - } else if (action === "restack_repo") { - await driver.stack.restackRepo(localPath); - } else if (action === "restack_subtree") { - await driver.stack.restackSubtree(localPath, branchName!); - } else if (action === "rebase_branch") { - await driver.stack.rebaseBranch(localPath, branchName!); - } else if (action === "reparent_branch") { - await driver.stack.reparentBranch(localPath, branchName!, parentBranch!); - } else { - throw new Error(`Unsupported repo stack action: ${action}`); - } - }); - - await forceProjectSync(c, localPath); - try { - const history = await getOrCreateHistory(c, c.state.workspaceId, c.state.repoId); - await history.append({ - kind: "repo.stack_action", - branchName: branchName ?? null, - payload: { - action, + await withRepoGitLock(localPath, async () => { + if (action === "sync_repo") { + await driver.stack.syncRepo(localPath); + } else if (action === "restack_repo") { + await driver.stack.restackRepo(localPath); + } else if (action === "restack_subtree") { + await driver.stack.restackSubtree(localPath, branchName!); + } else if (action === "rebase_branch") { + await driver.stack.rebaseBranch(localPath, branchName!); + } else if (action === "reparent_branch") { + await driver.stack.reparentBranch(localPath, branchName!, parentBranch!); + } else { + throw new Error(`Unsupported repo stack action: ${action}`); + } + }); + + try { + const history = await getOrCreateHistory(c, c.state.workspaceId, c.state.repoId); + await history.append({ + kind: "repo.stack_action", branchName: branchName ?? null, - parentBranch: parentBranch ?? null, - }, + payload: { + action, + branchName: branchName ?? null, + parentBranch: parentBranch ?? null, + jobId, + }, + }); + } catch (error) { + logActorWarning("project", "failed appending repo stack history event", { + workspaceId: c.state.workspaceId, + repoId: c.state.repoId, + action, + error: resolveErrorMessage(error), + }); + } + + await forceProjectSync(c, localPath); + + await writeRepoActionJob(c, { + jobId, + action, + branchName, + parentBranch, + status: "completed", + message: `Completed ${action}`, + createdAt: at, + completedAt: Date.now(), }); } catch (error) { - logActorWarning("project", "failed appending repo stack history event", { - workspaceId: c.state.workspaceId, - repoId: c.state.repoId, + const message = resolveErrorMessage(error); + await writeRepoActionJob(c, { + jobId, action, - error: resolveErrorMessage(error), + branchName, + parentBranch, + status: "error", + message, + createdAt: at, + completedAt: Date.now(), }); + throw error; } return { + jobId, action, executed: true, - message: `stack action executed: ${action}`, + status: "completed", + message: `Completed ${action}`, at, }; } @@ -999,7 +1134,6 @@ export const projectActions = { async getRepoOverview(c: any, _cmd?: RepoOverviewCommand): Promise { const localPath = await ensureProjectReadyForRead(c); await ensureTaskIndexHydratedForRead(c); - await forceProjectSync(c, localPath); const { driver } = getActorRuntimeContext(); const now = Date.now(); @@ -1118,6 +1252,9 @@ export const projectActions = { }; }); + const latestBranchSync = await c.db.select({ updatedAt: branches.updatedAt }).from(branches).orderBy(desc(branches.updatedAt)).limit(1).get(); + const latestPrSync = await c.db.select({ updatedAt: prCache.updatedAt }).from(prCache).orderBy(desc(prCache.updatedAt)).limit(1).get(); + return { workspaceId: c.state.workspaceId, repoId: c.state.repoId, @@ -1125,6 +1262,11 @@ export const projectActions = { baseRef, stackAvailable, fetchedAt: now, + branchSyncAt: latestBranchSync?.updatedAt ?? null, + prSyncAt: latestPrSync?.updatedAt ?? null, + branchSyncStatus: latestBranchSync ? "synced" : "pending", + prSyncStatus: latestPrSync ? "synced" : "pending", + repoActionJobs: await listRepoActionJobRows(c), branches: branchRows, }; }, @@ -1156,12 +1298,41 @@ export const projectActions = { async runRepoStackAction(c: any, cmd: RunRepoStackActionCommand): Promise { const self = selfProject(c); - return expectQueueResponse( - await self.send(projectWorkflowQueueName("project.command.runRepoStackAction"), cmd, { - wait: true, - timeout: 12 * 60_000, - }), + const jobId = randomUUID(); + const at = Date.now(); + const action = cmd.action; + const branchName = cmd.branchName?.trim() || null; + const parentBranch = cmd.parentBranch?.trim() || null; + + await writeRepoActionJob(c, { + jobId, + action, + branchName, + parentBranch, + status: "queued", + message: `Queued ${action}`, + createdAt: at, + }); + + await self.send( + projectWorkflowQueueName("project.command.runRepoStackAction"), + { + ...cmd, + jobId, + }, + { + wait: false, + }, ); + + return { + jobId, + action, + executed: true, + status: "queued", + message: `Queued ${action}`, + at, + }; }, async applyPrSyncResult(c: any, body: PrSyncResult): Promise { diff --git a/foundry/packages/backend/src/actors/project/db/schema.ts b/foundry/packages/backend/src/actors/project/db/schema.ts index b72cbfc..1ef4cee 100644 --- a/foundry/packages/backend/src/actors/project/db/schema.ts +++ b/foundry/packages/backend/src/actors/project/db/schema.ts @@ -42,3 +42,15 @@ export const taskIndex = sqliteTable("task_index", { createdAt: integer("created_at").notNull(), updatedAt: integer("updated_at").notNull(), }); + +export const repoActionJobs = sqliteTable("repo_action_jobs", { + jobId: text("job_id").notNull().primaryKey(), + action: text("action").notNull(), + branchName: text("branch_name"), + parentBranch: text("parent_branch"), + status: text("status").notNull(), + message: text("message").notNull(), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), + completedAt: integer("completed_at"), +}); diff --git a/foundry/packages/backend/src/actors/task/db/schema.ts b/foundry/packages/backend/src/actors/task/db/schema.ts index 2684ebb..2b59f4b 100644 --- a/foundry/packages/backend/src/actors/task/db/schema.ts +++ b/foundry/packages/backend/src/actors/task/db/schema.ts @@ -28,6 +28,10 @@ export const taskRuntime = sqliteTable( activeSwitchTarget: text("active_switch_target"), activeCwd: text("active_cwd"), statusMessage: text("status_message"), + gitStateJson: text("git_state_json"), + gitStateUpdatedAt: integer("git_state_updated_at"), + provisionStage: text("provision_stage"), + provisionStageUpdatedAt: integer("provision_stage_updated_at"), updatedAt: integer("updated_at").notNull(), }, (table) => [check("task_runtime_singleton_id_check", sql`${table.id} = 1`)], @@ -46,8 +50,13 @@ export const taskSandboxes = sqliteTable("task_sandboxes", { export const taskWorkbenchSessions = sqliteTable("task_workbench_sessions", { sessionId: text("session_id").notNull().primaryKey(), + sandboxSessionId: text("sandbox_session_id"), sessionName: text("session_name").notNull(), model: text("model").notNull(), + status: text("status").notNull().default("ready"), + errorMessage: text("error_message"), + transcriptJson: text("transcript_json").notNull().default("[]"), + transcriptUpdatedAt: integer("transcript_updated_at"), unread: integer("unread").notNull().default(0), draftText: text("draft_text").notNull().default(""), // Structured by the workbench composer attachment payload format. diff --git a/foundry/packages/backend/src/actors/task/workbench.ts b/foundry/packages/backend/src/actors/task/workbench.ts index fae749c..7de8f00 100644 --- a/foundry/packages/backend/src/actors/task/workbench.ts +++ b/foundry/packages/backend/src/actors/task/workbench.ts @@ -1,4 +1,5 @@ // @ts-nocheck +import { randomUUID } from "node:crypto"; import { basename } from "node:path"; import { asc, eq } from "drizzle-orm"; import { getActorRuntimeContext } from "../context.js"; @@ -9,12 +10,26 @@ import { getCurrentRecord } from "./workflow/common.js"; const STATUS_SYNC_INTERVAL_MS = 1_000; +function emptyGitState() { + return { + fileChanges: [], + diffs: {}, + fileTree: [], + updatedAt: null as number | null, + }; +} + async function ensureWorkbenchSessionTable(c: any): Promise { await c.db.execute(` CREATE TABLE IF NOT EXISTS task_workbench_sessions ( session_id text PRIMARY KEY NOT NULL, + sandbox_session_id text, session_name text NOT NULL, model text NOT NULL, + status text DEFAULT 'ready' NOT NULL, + error_message text, + transcript_json text DEFAULT '[]' NOT NULL, + transcript_updated_at integer, unread integer DEFAULT 0 NOT NULL, draft_text text DEFAULT '' NOT NULL, draft_attachments_json text DEFAULT '[]' NOT NULL, @@ -26,6 +41,18 @@ async function ensureWorkbenchSessionTable(c: any): Promise { updated_at integer NOT NULL ) `); + await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN sandbox_session_id text`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN status text DEFAULT 'ready' NOT NULL`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN error_message text`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN transcript_json text DEFAULT '[]' NOT NULL`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_workbench_sessions ADD COLUMN transcript_updated_at integer`).catch(() => {}); +} + +async function ensureTaskRuntimeCacheColumns(c: any): Promise { + await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN git_state_json text`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN git_state_updated_at integer`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN provision_stage text`).catch(() => {}); + await c.db.execute(`ALTER TABLE task_runtime ADD COLUMN provision_stage_updated_at integer`).catch(() => {}); } function defaultModelForAgent(agentType: string | null | undefined) { @@ -74,6 +101,40 @@ function parseDraftAttachments(value: string | null | undefined): Array { } } +function parseTranscript(value: string | null | undefined): Array { + if (!value) { + return []; + } + + try { + const parsed = JSON.parse(value) as unknown; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function parseGitState(value: string | null | undefined): { fileChanges: Array; diffs: Record; fileTree: Array } { + if (!value) { + return emptyGitState(); + } + + try { + const parsed = JSON.parse(value) as { + fileChanges?: unknown; + diffs?: unknown; + fileTree?: unknown; + }; + return { + fileChanges: Array.isArray(parsed.fileChanges) ? parsed.fileChanges : [], + diffs: parsed.diffs && typeof parsed.diffs === "object" ? (parsed.diffs as Record) : {}, + fileTree: Array.isArray(parsed.fileTree) ? parsed.fileTree : [], + }; + } catch { + return emptyGitState(); + } +} + export function shouldMarkSessionUnreadForStatus(meta: { thinkingSinceMs?: number | null }, status: "running" | "idle" | "error"): boolean { if (status === "running") { return false; @@ -90,7 +151,13 @@ async function listSessionMetaRows(c: any, options?: { includeClosed?: boolean } const mapped = rows.map((row: any) => ({ ...row, id: row.sessionId, - sessionId: row.sessionId, + sessionId: row.sandboxSessionId ?? null, + tabId: row.sessionId, + sandboxSessionId: row.sandboxSessionId ?? null, + status: row.status ?? "ready", + errorMessage: row.errorMessage ?? null, + transcript: parseTranscript(row.transcriptJson), + transcriptUpdatedAt: row.transcriptUpdatedAt ?? null, draftAttachments: parseDraftAttachments(row.draftAttachmentsJson), draftUpdatedAtMs: row.draftUpdatedAt ?? null, unread: row.unread === 1, @@ -121,7 +188,13 @@ async function readSessionMeta(c: any, sessionId: string): Promise { return { ...row, id: row.sessionId, - sessionId: row.sessionId, + sessionId: row.sandboxSessionId ?? null, + tabId: row.sessionId, + sandboxSessionId: row.sandboxSessionId ?? null, + status: row.status ?? "ready", + errorMessage: row.errorMessage ?? null, + transcript: parseTranscript(row.transcriptJson), + transcriptUpdatedAt: row.transcriptUpdatedAt ?? null, draftAttachments: parseDraftAttachments(row.draftAttachmentsJson), draftUpdatedAtMs: row.draftUpdatedAt ?? null, unread: row.unread === 1, @@ -133,14 +206,18 @@ async function readSessionMeta(c: any, sessionId: string): Promise { async function ensureSessionMeta( c: any, params: { - sessionId: string; + tabId: string; + sandboxSessionId?: string | null; model?: string; sessionName?: string; unread?: boolean; + created?: boolean; + status?: "pending_provision" | "pending_session_create" | "ready" | "error"; + errorMessage?: string | null; }, ): Promise { await ensureWorkbenchSessionTable(c); - const existing = await readSessionMeta(c, params.sessionId); + const existing = await readSessionMeta(c, params.tabId); if (existing) { return existing; } @@ -153,14 +230,19 @@ async function ensureSessionMeta( await c.db .insert(taskWorkbenchSessions) .values({ - sessionId: params.sessionId, + sessionId: params.tabId, + sandboxSessionId: params.sandboxSessionId ?? null, sessionName, model, + status: params.status ?? "ready", + errorMessage: params.errorMessage ?? null, + transcriptJson: "[]", + transcriptUpdatedAt: null, unread: unread ? 1 : 0, draftText: "", draftAttachmentsJson: "[]", draftUpdatedAt: null, - created: 1, + created: params.created === false ? 0 : 1, closed: 0, thinkingSinceMs: null, createdAt: now, @@ -168,20 +250,40 @@ async function ensureSessionMeta( }) .run(); - return await readSessionMeta(c, params.sessionId); + return await readSessionMeta(c, params.tabId); } -async function updateSessionMeta(c: any, sessionId: string, values: Record): Promise { - await ensureSessionMeta(c, { sessionId }); +async function updateSessionMeta(c: any, tabId: string, values: Record): Promise { + await ensureSessionMeta(c, { tabId }); await c.db .update(taskWorkbenchSessions) .set({ ...values, updatedAt: Date.now(), }) - .where(eq(taskWorkbenchSessions.sessionId, sessionId)) + .where(eq(taskWorkbenchSessions.sessionId, tabId)) .run(); - return await readSessionMeta(c, sessionId); + return await readSessionMeta(c, tabId); +} + +async function readSessionMetaBySandboxSessionId(c: any, sandboxSessionId: string): Promise { + await ensureWorkbenchSessionTable(c); + const row = await c.db.select().from(taskWorkbenchSessions).where(eq(taskWorkbenchSessions.sandboxSessionId, sandboxSessionId)).get(); + if (!row) { + return null; + } + return await readSessionMeta(c, row.sessionId); +} + +async function requireReadySessionMeta(c: any, tabId: string): Promise { + const meta = await readSessionMeta(c, tabId); + if (!meta) { + throw new Error(`Unknown workbench tab: ${tabId}`); + } + if (meta.status !== "ready" || !meta.sandboxSessionId) { + throw new Error(meta.errorMessage ?? "This workbench tab is still preparing"); + } + return meta; } async function notifyWorkbenchUpdated(c: any): Promise { @@ -333,17 +435,6 @@ async function collectWorkbenchGitState(c: any, record: any) { label: "git diff numstat", }); const numstat = parseNumstat(numstatResult.result); - const diffs: Record = {}; - - for (const row of statusRows) { - const diffResult = await executeInSandbox(c, { - sandboxId: activeSandboxId, - cwd, - command: `if git ls-files --error-unmatch -- ${JSON.stringify(row.path)} >/dev/null 2>&1; then git diff -- ${JSON.stringify(row.path)}; else git diff --no-index -- /dev/null ${JSON.stringify(row.path)} || true; fi`, - label: `git diff ${row.path}`, - }); - diffs[row.path] = diffResult.result; - } const filesResult = await executeInSandbox(c, { sandboxId: activeSandboxId, @@ -356,6 +447,17 @@ async function collectWorkbenchGitState(c: any, record: any) { .map((line) => line.trim()) .filter(Boolean); + const diffs: Record = {}; + for (const row of statusRows) { + const diffResult = await executeInSandbox(c, { + sandboxId: activeSandboxId, + cwd, + command: `git diff -- ${JSON.stringify(row.path)}`, + label: `git diff ${row.path}`, + }); + diffs[row.path] = diffResult.exitCode === 0 ? diffResult.result : ""; + } + return { fileChanges: statusRows.map((row) => { const counts = numstat.get(row.path) ?? { added: 0, removed: 0 }; @@ -371,6 +473,37 @@ async function collectWorkbenchGitState(c: any, record: any) { }; } +async function readCachedGitState(c: any): Promise<{ fileChanges: Array; diffs: Record; fileTree: Array; updatedAt: number | null }> { + await ensureTaskRuntimeCacheColumns(c); + const row = await c.db + .select({ + gitStateJson: taskRuntime.gitStateJson, + gitStateUpdatedAt: taskRuntime.gitStateUpdatedAt, + }) + .from(taskRuntime) + .where(eq(taskRuntime.id, 1)) + .get(); + const parsed = parseGitState(row?.gitStateJson); + return { + ...parsed, + updatedAt: row?.gitStateUpdatedAt ?? null, + }; +} + +async function writeCachedGitState(c: any, gitState: { fileChanges: Array; diffs: Record; fileTree: Array }): Promise { + await ensureTaskRuntimeCacheColumns(c); + const now = Date.now(); + await c.db + .update(taskRuntime) + .set({ + gitStateJson: JSON.stringify(gitState), + gitStateUpdatedAt: now, + updatedAt: now, + }) + .where(eq(taskRuntime.id, 1)) + .run(); +} + async function readSessionTranscript(c: any, record: any, sessionId: string) { const sandboxId = record.activeSandboxId ?? record.sandboxes?.[0]?.sandboxId ?? null; if (!sandboxId) { @@ -380,7 +513,7 @@ async function readSessionTranscript(c: any, record: any, sessionId: string) { const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, sandboxId); const page = await sandbox.listSessionEvents({ sessionId, - limit: 500, + limit: 100, }); return page.items.map((event: any) => ({ id: event.id, @@ -393,14 +526,50 @@ async function readSessionTranscript(c: any, record: any, sessionId: string) { })); } -async function activeSessionStatus(c: any, record: any, sessionId: string) { - if (record.activeSessionId !== sessionId || !record.activeSandboxId) { +async function writeSessionTranscript(c: any, tabId: string, transcript: Array): Promise { + await updateSessionMeta(c, tabId, { + transcriptJson: JSON.stringify(transcript), + transcriptUpdatedAt: Date.now(), + }); +} + +async function enqueueWorkbenchRefresh( + c: any, + command: "task.command.workbench.refresh_derived" | "task.command.workbench.refresh_session_transcript", + body: Record, +): Promise { + const self = selfTask(c); + await self.send(command, body, { wait: false }); +} + +async function maybeScheduleWorkbenchRefreshes(c: any, record: any, sessions: Array): Promise { + const gitState = await readCachedGitState(c); + if (record.activeSandboxId && !gitState.updatedAt) { + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {}); + } + + for (const session of sessions) { + if (session.closed || session.status !== "ready" || !session.sandboxSessionId || session.transcriptUpdatedAt) { + continue; + } + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", { + sessionId: session.sandboxSessionId, + }); + } +} + +function activeSessionStatus(record: any, sessionId: string) { + if (record.activeSessionId !== sessionId) { return "idle"; } - const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId); - const status = await sandbox.sessionStatus({ sessionId }); - return status.status; + if (record.status === "running") { + return "running"; + } + if (record.status === "error") { + return "error"; + } + return "idle"; } async function readPullRequestSummary(c: any, branchName: string | null) { @@ -417,12 +586,15 @@ async function readPullRequestSummary(c: any, branchName: string | null) { } export async function ensureWorkbenchSeeded(c: any): Promise { + await ensureTaskRuntimeCacheColumns(c); const record = await getCurrentRecord({ db: c.db, state: c.state }); if (record.activeSessionId) { await ensureSessionMeta(c, { - sessionId: record.activeSessionId, + tabId: record.activeSessionId, + sandboxSessionId: record.activeSessionId, model: defaultModelForAgent(record.agentType), sessionName: "Session 1", + status: "ready", }); } return record; @@ -430,35 +602,38 @@ export async function ensureWorkbenchSeeded(c: any): Promise { export async function getWorkbenchTask(c: any): Promise { const record = await ensureWorkbenchSeeded(c); - const gitState = await collectWorkbenchGitState(c, record); + const gitState = await readCachedGitState(c); const sessions = await listSessionMetaRows(c); + await maybeScheduleWorkbenchRefreshes(c, record, sessions); const tabs = []; for (const meta of sessions) { - const status = await activeSessionStatus(c, record, meta.sessionId); + const derivedSandboxSessionId = meta.sandboxSessionId ?? (meta.status === "pending_provision" && record.activeSessionId ? record.activeSessionId : null); + const sessionStatus = + meta.status === "ready" && derivedSandboxSessionId ? activeSessionStatus(record, derivedSandboxSessionId) : meta.status === "error" ? "error" : "idle"; let thinkingSinceMs = meta.thinkingSinceMs ?? null; let unread = Boolean(meta.unread); - if (thinkingSinceMs && status !== "running") { + if (thinkingSinceMs && sessionStatus !== "running") { thinkingSinceMs = null; unread = true; } tabs.push({ id: meta.id, - sessionId: meta.sessionId, + sessionId: derivedSandboxSessionId, sessionName: meta.sessionName, agent: agentKindForModel(meta.model), model: meta.model, - status, - thinkingSinceMs: status === "running" ? thinkingSinceMs : null, + status: sessionStatus, + thinkingSinceMs: sessionStatus === "running" ? thinkingSinceMs : null, unread, - created: Boolean(meta.created), + created: Boolean(meta.created || derivedSandboxSessionId), draft: { text: meta.draftText ?? "", attachments: Array.isArray(meta.draftAttachments) ? meta.draftAttachments : [], updatedAtMs: meta.draftUpdatedAtMs ?? null, }, - transcript: await readSessionTranscript(c, record, meta.sessionId), + transcript: meta.transcript ?? [], }); } @@ -479,6 +654,25 @@ export async function getWorkbenchTask(c: any): Promise { }; } +export async function refreshWorkbenchDerivedState(c: any): Promise { + const record = await ensureWorkbenchSeeded(c); + const gitState = await collectWorkbenchGitState(c, record); + await writeCachedGitState(c, gitState); + await notifyWorkbenchUpdated(c); +} + +export async function refreshWorkbenchSessionTranscript(c: any, sessionId: string): Promise { + const record = await ensureWorkbenchSeeded(c); + const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await readSessionMeta(c, sessionId)); + if (!meta?.sandboxSessionId) { + return; + } + + const transcript = await readSessionTranscript(c, record, meta.sandboxSessionId); + await writeSessionTranscript(c, meta.tabId, transcript); + await notifyWorkbenchUpdated(c); +} + export async function renameWorkbenchTask(c: any, value: string): Promise { const nextTitle = value.trim(); if (!nextTitle) { @@ -549,51 +743,157 @@ export async function renameWorkbenchBranch(c: any, value: string): Promise { - let record = await ensureWorkbenchSeeded(c); - if (!record.activeSandboxId) { - const providerId = record.providerId ?? c.state.providerId ?? getActorRuntimeContext().providers.defaultProviderId(); - await selfTask(c).provision({ providerId }); - record = await ensureWorkbenchSeeded(c); - } + const record = await ensureWorkbenchSeeded(c); if (record.activeSessionId) { const existingSessions = await listSessionMetaRows(c); if (existingSessions.length === 0) { await ensureSessionMeta(c, { - sessionId: record.activeSessionId, + tabId: record.activeSessionId, + sandboxSessionId: record.activeSessionId, model: model ?? defaultModelForAgent(record.agentType), sessionName: "Session 1", + status: "ready", }); await notifyWorkbenchUpdated(c); return { tabId: record.activeSessionId }; } } - if (!record.activeSandboxId) { - throw new Error("cannot create session without an active sandbox"); + const tabId = `tab-${randomUUID()}`; + await ensureSessionMeta(c, { + tabId, + model: model ?? defaultModelForAgent(record.agentType), + status: record.activeSandboxId ? "pending_session_create" : "pending_provision", + created: false, + }); + + const providerId = record.providerId ?? c.state.providerId ?? getActorRuntimeContext().providers.defaultProviderId(); + const self = selfTask(c); + if (!record.activeSandboxId && !String(record.status ?? "").startsWith("init_")) { + await self.send("task.command.provision", { providerId }, { wait: false }); } + await self.send( + "task.command.workbench.ensure_session", + { tabId, ...(model ? { model } : {}) }, + { + wait: false, + }, + ); + await notifyWorkbenchUpdated(c); + return { tabId }; +} + +export async function ensureWorkbenchSession(c: any, tabId: string, model?: string): Promise { + const meta = await readSessionMeta(c, tabId); + if (!meta || meta.closed) { + return; + } + + const record = await ensureWorkbenchSeeded(c); + if (!record.activeSandboxId) { + await updateSessionMeta(c, tabId, { + status: "pending_provision", + errorMessage: null, + }); + return; + } + + if (!meta.sandboxSessionId && record.activeSessionId && meta.status === "pending_provision") { + const existingTabForActiveSession = await readSessionMetaBySandboxSessionId(c, record.activeSessionId); + if (existingTabForActiveSession && existingTabForActiveSession.tabId !== tabId) { + await updateSessionMeta(c, existingTabForActiveSession.tabId, { + closed: 1, + }); + } + await updateSessionMeta(c, tabId, { + sandboxSessionId: record.activeSessionId, + status: "ready", + errorMessage: null, + created: 1, + }); + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", { + sessionId: record.activeSessionId, + }); + await notifyWorkbenchUpdated(c); + return; + } + + if (meta.sandboxSessionId) { + await updateSessionMeta(c, tabId, { + status: "ready", + errorMessage: null, + }); + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", { + sessionId: meta.sandboxSessionId, + }); + await notifyWorkbenchUpdated(c); + return; + } + const activeSandbox = (record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null; const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? null; if (!cwd) { - throw new Error("cannot create session without a sandbox cwd"); + await updateSessionMeta(c, tabId, { + status: "error", + errorMessage: "cannot create session without a sandbox cwd", + }); + await notifyWorkbenchUpdated(c); + return; } - const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId); - const created = await sandbox.createSession({ - prompt: "", - cwd, - agent: agentTypeForModel(model ?? defaultModelForAgent(record.agentType)), + await updateSessionMeta(c, tabId, { + status: "pending_session_create", + errorMessage: null, }); - if (!created.id) { - throw new Error(created.error ?? "sandbox-agent session creation failed"); + + try { + const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId); + const created = await sandbox.createSession({ + prompt: "", + cwd, + agent: agentTypeForModel(model ?? meta.model ?? defaultModelForAgent(record.agentType)), + }); + if (!created.id) { + throw new Error(created.error ?? "sandbox-agent session creation failed"); + } + + await updateSessionMeta(c, tabId, { + sandboxSessionId: created.id, + status: "ready", + errorMessage: null, + }); + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", { + sessionId: created.id, + }); + } catch (error) { + await updateSessionMeta(c, tabId, { + status: "error", + errorMessage: error instanceof Error ? error.message : String(error), + }); } - await ensureSessionMeta(c, { - sessionId: created.id, - model: model ?? defaultModelForAgent(record.agentType), - }); await notifyWorkbenchUpdated(c); - return { tabId: created.id }; +} + +export async function enqueuePendingWorkbenchSessions(c: any): Promise { + const self = selfTask(c); + const pending = (await listSessionMetaRows(c, { includeClosed: true })).filter( + (row) => row.closed !== true && row.status !== "ready" && row.status !== "error", + ); + + for (const row of pending) { + await self.send( + "task.command.workbench.ensure_session", + { + tabId: row.tabId, + model: row.model, + }, + { + wait: false, + }, + ); + } } export async function renameWorkbenchSession(c: any, sessionId: string, title: string): Promise { @@ -636,7 +936,7 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri throw new Error("cannot send message without an active sandbox"); } - await ensureSessionMeta(c, { sessionId }); + const meta = await requireReadySessionMeta(c, sessionId); const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId); const prompt = [text.trim(), ...attachments.map((attachment: any) => `@ ${attachment.filePath}:${attachment.lineNumber}\n${attachment.lineContent}`)] .filter(Boolean) @@ -646,7 +946,7 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri } await sandbox.sendPrompt({ - sessionId, + sessionId: meta.sandboxSessionId, prompt, notification: true, }); @@ -663,24 +963,27 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri await c.db .update(taskRuntime) .set({ - activeSessionId: sessionId, + activeSessionId: meta.sandboxSessionId, updatedAt: Date.now(), }) .where(eq(taskRuntime.id, 1)) .run(); - const sync = await getOrCreateTaskStatusSync(c, c.state.workspaceId, c.state.repoId, c.state.taskId, record.activeSandboxId, sessionId, { + const sync = await getOrCreateTaskStatusSync(c, c.state.workspaceId, c.state.repoId, c.state.taskId, record.activeSandboxId, meta.sandboxSessionId, { workspaceId: c.state.workspaceId, repoId: c.state.repoId, taskId: c.state.taskId, providerId: c.state.providerId, sandboxId: record.activeSandboxId, - sessionId, + sessionId: meta.sandboxSessionId, intervalMs: STATUS_SYNC_INTERVAL_MS, }); await sync.setIntervalMs({ intervalMs: STATUS_SYNC_INTERVAL_MS }); await sync.start(); await sync.force(); + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", { + sessionId: meta.sandboxSessionId, + }); await notifyWorkbenchUpdated(c); } @@ -689,8 +992,9 @@ export async function stopWorkbenchSession(c: any, sessionId: string): Promise { const record = await ensureWorkbenchSeeded(c); - const meta = await ensureSessionMeta(c, { sessionId }); + const meta = (await readSessionMetaBySandboxSessionId(c, sessionId)) ?? (await ensureSessionMeta(c, { tabId: sessionId, sandboxSessionId: sessionId })); let changed = false; - if (record.activeSessionId === sessionId) { + if (record.activeSessionId === sessionId || record.activeSessionId === meta.sandboxSessionId) { const mappedStatus = status === "running" ? "running" : status === "error" ? "error" : "idle"; if (record.status !== mappedStatus) { await c.db @@ -753,27 +1057,36 @@ export async function syncWorkbenchSessionStatus(c: any, sessionId: string, stat } if (changed) { + if (status !== "running") { + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_session_transcript", { + sessionId, + }); + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {}); + } await notifyWorkbenchUpdated(c); } } export async function closeWorkbenchSession(c: any, sessionId: string): Promise { const record = await ensureWorkbenchSeeded(c); - if (!record.activeSandboxId) { - return; - } const sessions = await listSessionMetaRows(c); if (sessions.filter((candidate) => candidate.closed !== true).length <= 1) { return; } - const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId); - await sandbox.destroySession({ sessionId }); + const meta = await readSessionMeta(c, sessionId); + if (!meta) { + return; + } + if (record.activeSandboxId && meta.sandboxSessionId) { + const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId); + await sandbox.destroySession({ sessionId: meta.sandboxSessionId }); + } await updateSessionMeta(c, sessionId, { closed: 1, thinkingSinceMs: null, }); - if (record.activeSessionId === sessionId) { + if (record.activeSessionId === sessionId || record.activeSessionId === meta.sandboxSessionId) { await c.db .update(taskRuntime) .set({ @@ -792,7 +1105,7 @@ export async function markWorkbenchUnread(c: any): Promise { if (!latest) { return; } - await updateSessionMeta(c, latest.sessionId, { + await updateSessionMeta(c, latest.tabId, { unread: 1, }); await notifyWorkbenchUpdated(c); @@ -838,5 +1151,6 @@ export async function revertWorkbenchFile(c: any, path: string): Promise { if (result.exitCode !== 0) { throw new Error(`file revert failed (${result.exitCode}): ${result.result}`); } + await enqueueWorkbenchRefresh(c, "task.command.workbench.refresh_derived", {}); await notifyWorkbenchUpdated(c); } diff --git a/foundry/packages/backend/src/actors/task/workflow/index.ts b/foundry/packages/backend/src/actors/task/workflow/index.ts index e2da35c..de99ac1 100644 --- a/foundry/packages/backend/src/actors/task/workflow/index.ts +++ b/foundry/packages/backend/src/actors/task/workflow/index.ts @@ -8,6 +8,7 @@ import { initCompleteActivity, initCreateSandboxActivity, initCreateSessionActivity, + initEnqueueProvisionActivity, initEnsureAgentActivity, initEnsureNameActivity, initExposeSandboxActivity, @@ -32,6 +33,9 @@ import { changeWorkbenchModel, closeWorkbenchSession, createWorkbenchSession, + ensureWorkbenchSession, + refreshWorkbenchDerivedState, + refreshWorkbenchSessionTranscript, markWorkbenchUnread, publishWorkbenchPr, renameWorkbenchBranch, @@ -56,7 +60,7 @@ const commandHandlers: Record = { const body = msg.body; await loopCtx.step("init-bootstrap-db", async () => initBootstrapDbActivity(loopCtx, body)); - await loopCtx.removed("init-enqueue-provision", "step"); + await loopCtx.step("init-enqueue-provision", async () => initEnqueueProvisionActivity(loopCtx, body)); await loopCtx.removed("init-dispatch-provision-v2", "step"); const currentRecord = await loopCtx.step("init-read-current-record", async () => getCurrentRecord(loopCtx)); @@ -166,12 +170,21 @@ const commandHandlers: Record = { "task.command.workbench.create_session": async (loopCtx, msg) => { const created = await loopCtx.step({ name: "workbench-create-session", - timeout: 5 * 60_000, + timeout: 30_000, run: async () => createWorkbenchSession(loopCtx, msg.body?.model), }); await msg.complete(created); }, + "task.command.workbench.ensure_session": async (loopCtx, msg) => { + await loopCtx.step({ + name: "workbench-ensure-session", + timeout: 5 * 60_000, + run: async () => ensureWorkbenchSession(loopCtx, msg.body.tabId, msg.body?.model), + }); + await msg.complete({ ok: true }); + }, + "task.command.workbench.rename_session": async (loopCtx, msg) => { await loopCtx.step("workbench-rename-session", async () => renameWorkbenchSession(loopCtx, msg.body.sessionId, msg.body.title)); await msg.complete({ ok: true }); @@ -215,6 +228,24 @@ const commandHandlers: Record = { await msg.complete({ ok: true }); }, + "task.command.workbench.refresh_derived": async (loopCtx, msg) => { + await loopCtx.step({ + name: "workbench-refresh-derived", + timeout: 5 * 60_000, + run: async () => refreshWorkbenchDerivedState(loopCtx), + }); + await msg.complete({ ok: true }); + }, + + "task.command.workbench.refresh_session_transcript": async (loopCtx, msg) => { + await loopCtx.step({ + name: "workbench-refresh-session-transcript", + timeout: 60_000, + run: async () => refreshWorkbenchSessionTranscript(loopCtx, msg.body.sessionId), + }); + await msg.complete({ ok: true }); + }, + "task.command.workbench.close_session": async (loopCtx, msg) => { await loopCtx.step({ name: "workbench-close-session", diff --git a/foundry/packages/backend/src/actors/task/workflow/init.ts b/foundry/packages/backend/src/actors/task/workflow/init.ts index 922f5d9..8629cd8 100644 --- a/foundry/packages/backend/src/actors/task/workflow/init.ts +++ b/foundry/packages/backend/src/actors/task/workflow/init.ts @@ -8,6 +8,7 @@ import { logActorWarning, resolveErrorMessage } from "../../logging.js"; import { task as taskTable, taskRuntime, taskSandboxes } from "../db/schema.js"; import { TASK_ROW_ID, appendHistory, buildAgentPrompt, collectErrorMessages, resolveErrorDetail, setTaskState } from "./common.js"; import { taskWorkflowQueueName } from "./queue.js"; +import { enqueuePendingWorkbenchSessions } from "../workbench.js"; const DEFAULT_INIT_CREATE_SANDBOX_ACTIVITY_TIMEOUT_MS = 180_000; @@ -96,6 +97,10 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise< activeSwitchTarget: null, activeCwd: null, statusMessage: initialStatusMessage, + gitStateJson: null, + gitStateUpdatedAt: null, + provisionStage: "queued", + provisionStageUpdatedAt: now, updatedAt: now, }) .onConflictDoUpdate({ @@ -106,6 +111,8 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise< activeSwitchTarget: null, activeCwd: null, statusMessage: initialStatusMessage, + provisionStage: "queued", + provisionStageUpdatedAt: now, updatedAt: now, }, }) @@ -118,19 +125,29 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise< export async function initEnqueueProvisionActivity(loopCtx: any, body: any): Promise { await setTaskState(loopCtx, "init_enqueue_provision", "provision queued"); - const self = selfTask(loopCtx); - void self - .send(taskWorkflowQueueName("task.command.provision"), body, { - wait: false, + await loopCtx.db + .update(taskRuntime) + .set({ + provisionStage: "queued", + provisionStageUpdatedAt: Date.now(), + updatedAt: Date.now(), }) - .catch((error: unknown) => { - logActorWarning("task.init", "background provision command failed", { - workspaceId: loopCtx.state.workspaceId, - repoId: loopCtx.state.repoId, - taskId: loopCtx.state.taskId, - error: resolveErrorMessage(error), - }); + .where(eq(taskRuntime.id, TASK_ROW_ID)) + .run(); + const self = selfTask(loopCtx); + try { + await self.send(taskWorkflowQueueName("task.command.provision"), body, { + wait: false, }); + } catch (error: unknown) { + logActorWarning("task.init", "background provision command failed", { + workspaceId: loopCtx.state.workspaceId, + repoId: loopCtx.state.repoId, + taskId: loopCtx.state.taskId, + error: resolveErrorMessage(error), + }); + throw error; + } } export async function initEnsureNameActivity(loopCtx: any): Promise { @@ -197,6 +214,8 @@ export async function initEnsureNameActivity(loopCtx: any): Promise { .update(taskRuntime) .set({ statusMessage: "provisioning", + provisionStage: "repo_prepared", + provisionStageUpdatedAt: now, updatedAt: now, }) .where(eq(taskRuntime.id, TASK_ROW_ID)) @@ -222,6 +241,15 @@ export async function initAssertNameActivity(loopCtx: any): Promise { export async function initCreateSandboxActivity(loopCtx: any, body: any): Promise { await setTaskState(loopCtx, "init_create_sandbox", "creating sandbox"); + await loopCtx.db + .update(taskRuntime) + .set({ + provisionStage: "sandbox_allocated", + provisionStageUpdatedAt: Date.now(), + updatedAt: Date.now(), + }) + .where(eq(taskRuntime.id, TASK_ROW_ID)) + .run(); const { providers } = getActorRuntimeContext(); const providerId = body?.providerId ?? loopCtx.state.providerId; const provider = providers.get(providerId); @@ -307,6 +335,15 @@ export async function initCreateSandboxActivity(loopCtx: any, body: any): Promis export async function initEnsureAgentActivity(loopCtx: any, body: any, sandbox: any): Promise { await setTaskState(loopCtx, "init_ensure_agent", "ensuring sandbox agent"); + await loopCtx.db + .update(taskRuntime) + .set({ + provisionStage: "agent_installing", + provisionStageUpdatedAt: Date.now(), + updatedAt: Date.now(), + }) + .where(eq(taskRuntime.id, TASK_ROW_ID)) + .run(); const { providers } = getActorRuntimeContext(); const providerId = body?.providerId ?? loopCtx.state.providerId; const provider = providers.get(providerId); @@ -318,6 +355,15 @@ export async function initEnsureAgentActivity(loopCtx: any, body: any, sandbox: export async function initStartSandboxInstanceActivity(loopCtx: any, body: any, sandbox: any, agent: any): Promise { await setTaskState(loopCtx, "init_start_sandbox_instance", "starting sandbox runtime"); + await loopCtx.db + .update(taskRuntime) + .set({ + provisionStage: "agent_starting", + provisionStageUpdatedAt: Date.now(), + updatedAt: Date.now(), + }) + .where(eq(taskRuntime.id, TASK_ROW_ID)) + .run(); try { const providerId = body?.providerId ?? loopCtx.state.providerId; const sandboxInstance = await getOrCreateSandboxInstance(loopCtx, loopCtx.state.workspaceId, providerId, sandbox.sandboxId, { @@ -350,6 +396,15 @@ export async function initStartSandboxInstanceActivity(loopCtx: any, body: any, export async function initCreateSessionActivity(loopCtx: any, body: any, sandbox: any, sandboxInstanceReady: any): Promise { await setTaskState(loopCtx, "init_create_session", "creating agent session"); + await loopCtx.db + .update(taskRuntime) + .set({ + provisionStage: "session_creating", + provisionStageUpdatedAt: Date.now(), + updatedAt: Date.now(), + }) + .where(eq(taskRuntime.id, TASK_ROW_ID)) + .run(); if (!sandboxInstanceReady.ok) { return { id: null, @@ -481,6 +536,8 @@ export async function initWriteDbActivity( activeSwitchTarget: sandbox.switchTarget, activeCwd, statusMessage, + provisionStage: sessionHealthy ? "ready" : "error", + provisionStageUpdatedAt: now, updatedAt: now, }) .onConflictDoUpdate({ @@ -491,6 +548,8 @@ export async function initWriteDbActivity( activeSwitchTarget: sandbox.switchTarget, activeCwd, statusMessage, + provisionStage: sessionHealthy ? "ready" : "error", + provisionStageUpdatedAt: now, updatedAt: now, }, }) @@ -535,6 +594,12 @@ export async function initCompleteActivity(loopCtx: any, body: any, sandbox: any }); loopCtx.state.initialized = true; + await enqueuePendingWorkbenchSessions(loopCtx); + const self = selfTask(loopCtx); + await self.send(taskWorkflowQueueName("task.command.workbench.refresh_derived"), {}, { wait: false }); + if (sessionId) { + await self.send(taskWorkflowQueueName("task.command.workbench.refresh_session_transcript"), { sessionId }, { wait: false }); + } return; } @@ -591,6 +656,8 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise< activeSwitchTarget: null, activeCwd: null, statusMessage: detail, + provisionStage: "error", + provisionStageUpdatedAt: now, updatedAt: now, }) .onConflictDoUpdate({ @@ -601,6 +668,8 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise< activeSwitchTarget: null, activeCwd: null, statusMessage: detail, + provisionStage: "error", + provisionStageUpdatedAt: now, updatedAt: now, }, }) diff --git a/foundry/packages/backend/src/actors/task/workflow/queue.ts b/foundry/packages/backend/src/actors/task/workflow/queue.ts index 399414b..db5c0a3 100644 --- a/foundry/packages/backend/src/actors/task/workflow/queue.ts +++ b/foundry/packages/backend/src/actors/task/workflow/queue.ts @@ -13,6 +13,7 @@ export const TASK_QUEUE_NAMES = [ "task.command.workbench.rename_task", "task.command.workbench.rename_branch", "task.command.workbench.create_session", + "task.command.workbench.ensure_session", "task.command.workbench.rename_session", "task.command.workbench.set_session_unread", "task.command.workbench.update_draft", @@ -20,6 +21,8 @@ export const TASK_QUEUE_NAMES = [ "task.command.workbench.send_message", "task.command.workbench.stop_session", "task.command.workbench.sync_session_status", + "task.command.workbench.refresh_derived", + "task.command.workbench.refresh_session_transcript", "task.command.workbench.close_session", "task.command.workbench.publish_pr", "task.command.workbench.revert_file", diff --git a/foundry/packages/backend/src/actors/workspace/actions.ts b/foundry/packages/backend/src/actors/workspace/actions.ts index d6ac94b..51843fb 100644 --- a/foundry/packages/backend/src/actors/workspace/actions.ts +++ b/foundry/packages/backend/src/actors/workspace/actions.ts @@ -306,9 +306,6 @@ async function createTaskMutation(c: any, input: CreateTaskInput): Promise normalizeAuthValue(entry)); + } + return value; +} + +function workspaceAuthClause(table: any, clause: { field: string; value: unknown; operator?: string }): any { + const column = workspaceAuthColumn(table, clause.field); + const value = normalizeAuthValue(clause.value); + switch (clause.operator) { + case "ne": + return value === null ? isNotNull(column) : ne(column, value as any); + case "lt": + return lt(column, value as any); + case "lte": + return lte(column, value as any); + case "gt": + return gt(column, value as any); + case "gte": + return gte(column, value as any); + case "in": + return inArray(column, Array.isArray(value) ? (value as any[]) : [value as any]); + case "not_in": + return notInArray(column, Array.isArray(value) ? (value as any[]) : [value as any]); + case "contains": + return like(column, `%${String(value ?? "")}%`); + case "starts_with": + return like(column, `${String(value ?? "")}%`); + case "ends_with": + return like(column, `%${String(value ?? "")}`); + case "eq": + default: + return value === null ? isNull(column) : eq(column, value as any); + } +} + +function workspaceAuthWhere(table: any, clauses: any[] | undefined): any { + if (!clauses || clauses.length === 0) { + return undefined; + } + let expr = workspaceAuthClause(table, clauses[0]); + for (const clause of clauses.slice(1)) { + const next = workspaceAuthClause(table, clause); + expr = clause.connector === "OR" ? or(expr, next) : and(expr, next); + } + return expr; +} + const githubWebhookLogger = logger.child({ scope: "github-webhook", }); const PROFILE_ROW_ID = "profile"; -const OAUTH_TTL_MS = 10 * 60_000; function roundDurationMs(start: number): number { return Math.round((performance.now() - start) * 100) / 100; @@ -58,13 +132,6 @@ function organizationWorkspaceId(kind: FoundryOrganization["kind"], login: strin return kind === "personal" ? personalWorkspaceId(login) : slugify(login); } -function splitScopes(value: string): string[] { - return value - .split(",") - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0); -} - function hasRepoScope(scopes: string[]): boolean { return scopes.some((scope) => scope === "repo" || scope.startsWith("repo:")); } @@ -85,21 +152,6 @@ function encodeEligibleOrganizationIds(value: string[]): string { return JSON.stringify([...new Set(value)]); } -function encodeOauthState(payload: { sessionId: string; nonce: string }): string { - return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); -} - -function decodeOauthState(value: string): { sessionId: string; nonce: string } { - const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as Record; - if (typeof parsed.sessionId !== "string" || typeof parsed.nonce !== "string") { - throw new Error("GitHub OAuth state is malformed"); - } - return { - sessionId: parsed.sessionId, - nonce: parsed.nonce, - }; -} - function seatsIncludedForPlan(planId: FoundryBillingPlanId): number { switch (planId) { case "free": @@ -161,70 +213,6 @@ function stripeWebhookSubscription(event: any) { }; } -async function getAppSessionRow(c: any, sessionId: string) { - assertAppWorkspace(c); - return await c.db.select().from(appSessions).where(eq(appSessions.id, sessionId)).get(); -} - -async function requireAppSessionRow(c: any, sessionId: string) { - const row = await getAppSessionRow(c, sessionId); - if (!row) { - throw new Error(`Unknown app session: ${sessionId}`); - } - return row; -} - -async function ensureAppSession(c: any, requestedSessionId?: string | null): Promise { - assertAppWorkspace(c); - const requested = typeof requestedSessionId === "string" && requestedSessionId.trim().length > 0 ? requestedSessionId.trim() : null; - - if (requested) { - const existing = await getAppSessionRow(c, requested); - if (existing) { - return requested; - } - } - - const sessionId = requested ?? randomUUID(); - const now = Date.now(); - await c.db - .insert(appSessions) - .values({ - id: sessionId, - currentUserId: null, - currentUserName: null, - currentUserEmail: null, - currentUserGithubLogin: null, - currentUserRoleLabel: null, - eligibleOrganizationIdsJson: "[]", - activeOrganizationId: null, - githubAccessToken: null, - githubScope: "", - starterRepoStatus: "pending", - starterRepoStarredAt: null, - starterRepoSkippedAt: null, - oauthState: null, - oauthStateExpiresAt: null, - createdAt: now, - updatedAt: now, - }) - .onConflictDoNothing() - .run(); - return sessionId; -} - -async function updateAppSession(c: any, sessionId: string, patch: Record): Promise { - assertAppWorkspace(c); - await c.db - .update(appSessions) - .set({ - ...patch, - updatedAt: Date.now(), - }) - .where(eq(appSessions.id, sessionId)) - .run(); -} - async function getOrganizationState(workspace: any) { return await workspace.getOrganizationShellState({}); } @@ -232,8 +220,27 @@ async function getOrganizationState(workspace: any) { async function buildAppSnapshot(c: any, sessionId: string): Promise { assertAppWorkspace(c); const startedAt = performance.now(); - const session = await requireAppSessionRow(c, sessionId); - const eligibleOrganizationIds = parseEligibleOrganizationIds(session.eligibleOrganizationIdsJson); + const auth = getBetterAuthService(); + let authState = await auth.getAuthState(sessionId); + // Inline fallback: if the user is signed in but has no eligible organizations yet + // (e.g. first load after OAuth callback), sync GitHub orgs before building the snapshot. + if (authState?.user && parseEligibleOrganizationIds(authState.profile?.eligibleOrganizationIdsJson ?? "[]").length === 0) { + const token = await auth.getAccessTokenForSession(sessionId); + if (token?.accessToken) { + logger.info({ sessionId }, "build_app_snapshot_sync_orgs"); + await syncGithubOrganizations(c, { sessionId, accessToken: token.accessToken }); + authState = await auth.getAuthState(sessionId); + } else { + logger.warn({ sessionId }, "build_app_snapshot_no_access_token"); + } + } + + const session = authState?.session ?? null; + const user = authState?.user ?? null; + const profile = authState?.profile ?? null; + const currentSessionState = authState?.sessionState ?? null; + const githubAccount = authState?.accounts?.find((account: any) => account.providerId === "github") ?? null; + const eligibleOrganizationIds = parseEligibleOrganizationIds(profile?.eligibleOrganizationIdsJson ?? "[]"); logger.info( { @@ -293,25 +300,27 @@ async function buildAppSnapshot(c: any, sessionId: string): Promise organization !== null); - const currentUser: FoundryUser | null = session.currentUserId + const currentUser: FoundryUser | null = user ? { - id: session.currentUserId, - name: session.currentUserName ?? session.currentUserGithubLogin ?? "GitHub user", - email: session.currentUserEmail ?? "", - githubLogin: session.currentUserGithubLogin ?? "", - roleLabel: session.currentUserRoleLabel ?? "GitHub user", - eligibleOrganizationIds: organizations.map((organization) => organization.id), + id: profile?.githubAccountId ?? githubAccount?.accountId ?? user.id, + name: user.name, + email: user.email, + githubLogin: profile?.githubLogin ?? "", + roleLabel: profile?.roleLabel ?? "GitHub user", + eligibleOrganizationIds, } : null; const activeOrganizationId = - currentUser && session.activeOrganizationId && organizations.some((organization) => organization.id === session.activeOrganizationId) - ? session.activeOrganizationId + currentUser && + currentSessionState?.activeOrganizationId && + organizations.some((organization) => organization.id === currentSessionState.activeOrganizationId) + ? currentSessionState.activeOrganizationId : currentUser && organizations.length === 1 ? (organizations[0]?.id ?? null) : null; - const snapshot = { + const snapshot: FoundryAppSnapshot = { auth: { status: currentUser ? "signed_in" : "signed_out", currentUserId: currentUser?.id ?? null, @@ -321,9 +330,9 @@ async function buildAppSnapshot(c: any, sessionId: string): Promise account.providerId === "github") ?? null; + if (!authState?.session || !user?.email) { throw new Error("User must be signed in"); } - return session; + const token = await auth.getAccessTokenForSession(sessionId); + return { + ...authState.session, + authUserId: user.id, + currentUserId: profile?.githubAccountId ?? githubAccount?.accountId ?? user.id, + currentUserName: user.name, + currentUserEmail: user.email, + currentUserGithubLogin: profile?.githubLogin ?? "", + currentUserRoleLabel: profile?.roleLabel ?? "GitHub user", + eligibleOrganizationIdsJson: profile?.eligibleOrganizationIdsJson ?? "[]", + githubAccessToken: token?.accessToken ?? null, + githubScope: (token?.scopes ?? []).join(","), + starterRepoStatus: profile?.starterRepoStatus ?? "pending", + starterRepoStarredAt: profile?.starterRepoStarredAt ?? null, + starterRepoSkippedAt: profile?.starterRepoSkippedAt ?? null, + }; } function requireEligibleOrganization(session: any, organizationId: string): void { @@ -431,54 +459,26 @@ async function safeListInstallations(accessToken: string): Promise { } } -/** - * Fast path: resolve viewer identity, store user + token in the session, - * and return the redirect URL. Does NOT sync organizations — that work is - * deferred to `syncGithubOrganizations` via the workflow queue so the HTTP - * callback can respond before any proxy timeout triggers a retry. - */ -async function initGithubSession(c: any, sessionId: string, accessToken: string, scopes: string[]): Promise<{ sessionId: string; redirectTo: string }> { - assertAppWorkspace(c); - const { appShell } = getActorRuntimeContext(); - const viewer = await appShell.github.getViewer(accessToken); - const userId = `user-${slugify(viewer.login)}`; - - await updateAppSession(c, sessionId, { - currentUserId: userId, - currentUserName: viewer.name || viewer.login, - currentUserEmail: viewer.email ?? `${viewer.login}@users.noreply.github.com`, - currentUserGithubLogin: viewer.login, - currentUserRoleLabel: "GitHub user", - githubAccessToken: accessToken, - githubScope: scopes.join(","), - oauthState: null, - oauthStateExpiresAt: null, - }); - - return { - sessionId, - redirectTo: `${appShell.appUrl}/organizations?foundrySession=${encodeURIComponent(sessionId)}`, - }; -} - /** * Slow path: list GitHub orgs + installations, sync each org workspace, * and update the session's eligible organization list. Called from the * workflow queue so it runs in the background after the callback has * already returned a redirect to the browser. - * - * Also used synchronously by bootstrapAppGithubSession (dev-only) where - * proxy timeouts are not a concern. */ export async function syncGithubOrganizations(c: any, input: { sessionId: string; accessToken: string }): Promise { assertAppWorkspace(c); + const auth = getBetterAuthService(); const { appShell } = getActorRuntimeContext(); const { sessionId, accessToken } = input; - const session = await requireAppSessionRow(c, sessionId); + const authState = await auth.getAuthState(sessionId); + if (!authState?.user) { + throw new Error("User must be signed in"); + } const viewer = await appShell.github.getViewer(accessToken); const organizations = await safeListOrganizations(accessToken); const installations = await safeListInstallations(accessToken); - const userId = `user-${slugify(viewer.login)}`; + const authUserId = authState.user.id; + const githubUserId = String(viewer.id); const linkedOrganizationIds: string[] = []; const accounts = [ @@ -503,7 +503,7 @@ export async function syncGithubOrganizations(c: any, input: { sessionId: string const installation = installations.find((candidate) => candidate.accountLogin === account.githubLogin) ?? null; const workspace = await getOrCreateWorkspace(c, organizationId); await workspace.syncOrganizationShellFromGithub({ - userId, + userId: githubUserId, userName: viewer.name || viewer.login, userEmail: viewer.email ?? `${viewer.login}@users.noreply.github.com`, githubUserLogin: viewer.login, @@ -519,16 +519,20 @@ export async function syncGithubOrganizations(c: any, input: { sessionId: string } const activeOrganizationId = - session.activeOrganizationId && linkedOrganizationIds.includes(session.activeOrganizationId) - ? session.activeOrganizationId + authState.sessionState?.activeOrganizationId && linkedOrganizationIds.includes(authState.sessionState.activeOrganizationId) + ? authState.sessionState.activeOrganizationId : linkedOrganizationIds.length === 1 ? (linkedOrganizationIds[0] ?? null) : null; - await updateAppSession(c, sessionId, { + await auth.setActiveOrganization(sessionId, activeOrganizationId); + await auth.upsertUserProfile(authUserId, { + githubAccountId: String(viewer.id), + githubLogin: viewer.login, + roleLabel: "GitHub user", eligibleOrganizationIdsJson: encodeEligibleOrganizationIds(linkedOrganizationIds), - activeOrganizationId, }); + c.broadcast("appUpdated", { at: Date.now(), sessionId }); } export async function syncGithubOrganizationRepos(c: any, input: { sessionId: string; organizationId: string }): Promise { @@ -583,19 +587,6 @@ export async function syncGithubOrganizationRepos(c: any, input: { sessionId: st } } -/** - * Full synchronous sync: init session + sync orgs in one call. - * Used by bootstrapAppGithubSession (dev-only) where there is no proxy - * timeout concern and we want the session fully populated before returning. - */ -async function syncGithubSessionFromToken(c: any, sessionId: string, accessToken: string): Promise<{ sessionId: string; redirectTo: string }> { - const session = await requireAppSessionRow(c, sessionId); - const scopes = splitScopes(session.githubScope); - const result = await initGithubSession(c, sessionId, accessToken, scopes); - await syncGithubOrganizations(c, { sessionId, accessToken }); - return result; -} - async function readOrganizationProfileRow(c: any) { assertOrganizationWorkspace(c); return await c.db.select().from(organizationProfile).where(eq(organizationProfile.id, PROFILE_ROW_ID)).get(); @@ -736,9 +727,253 @@ async function applySubscriptionState( } export const workspaceAppActions = { - async ensureAppSession(c: any, input?: { requestedSessionId?: string | null }): Promise<{ sessionId: string }> { - const sessionId = await ensureAppSession(c, input?.requestedSessionId); - return { sessionId }; + async authFindSessionIndex(c: any, input: { sessionId?: string; sessionToken?: string }) { + assertAppWorkspace(c); + + const clauses = [ + ...(input.sessionId ? [{ field: "sessionId", value: input.sessionId }] : []), + ...(input.sessionToken ? [{ field: "sessionToken", value: input.sessionToken }] : []), + ]; + if (clauses.length === 0) { + return null; + } + const predicate = workspaceAuthWhere(authSessionIndex, clauses); + return await c.db.select().from(authSessionIndex).where(predicate!).get(); + }, + + async authUpsertSessionIndex(c: any, input: { sessionId: string; sessionToken: string; userId: string }) { + assertAppWorkspace(c); + + const now = Date.now(); + await c.db + .insert(authSessionIndex) + .values({ + sessionId: input.sessionId, + sessionToken: input.sessionToken, + userId: input.userId, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: authSessionIndex.sessionId, + set: { + sessionToken: input.sessionToken, + userId: input.userId, + updatedAt: now, + }, + }) + .run(); + return await c.db.select().from(authSessionIndex).where(eq(authSessionIndex.sessionId, input.sessionId)).get(); + }, + + async authDeleteSessionIndex(c: any, input: { sessionId?: string; sessionToken?: string }) { + assertAppWorkspace(c); + + const clauses = [ + ...(input.sessionId ? [{ field: "sessionId", value: input.sessionId }] : []), + ...(input.sessionToken ? [{ field: "sessionToken", value: input.sessionToken }] : []), + ]; + if (clauses.length === 0) { + return; + } + const predicate = workspaceAuthWhere(authSessionIndex, clauses); + await c.db.delete(authSessionIndex).where(predicate!).run(); + }, + + async authFindEmailIndex(c: any, input: { email: string }) { + assertAppWorkspace(c); + + return await c.db.select().from(authEmailIndex).where(eq(authEmailIndex.email, input.email)).get(); + }, + + async authUpsertEmailIndex(c: any, input: { email: string; userId: string }) { + assertAppWorkspace(c); + + const now = Date.now(); + await c.db + .insert(authEmailIndex) + .values({ + email: input.email, + userId: input.userId, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: authEmailIndex.email, + set: { + userId: input.userId, + updatedAt: now, + }, + }) + .run(); + return await c.db.select().from(authEmailIndex).where(eq(authEmailIndex.email, input.email)).get(); + }, + + async authDeleteEmailIndex(c: any, input: { email: string }) { + assertAppWorkspace(c); + + await c.db.delete(authEmailIndex).where(eq(authEmailIndex.email, input.email)).run(); + }, + + async authFindAccountIndex(c: any, input: { id?: string; providerId?: string; accountId?: string }) { + assertAppWorkspace(c); + + if (input.id) { + return await c.db.select().from(authAccountIndex).where(eq(authAccountIndex.id, input.id)).get(); + } + if (!input.providerId || !input.accountId) { + return null; + } + return await c.db + .select() + .from(authAccountIndex) + .where(and(eq(authAccountIndex.providerId, input.providerId), eq(authAccountIndex.accountId, input.accountId))) + .get(); + }, + + async authUpsertAccountIndex(c: any, input: { id: string; providerId: string; accountId: string; userId: string }) { + assertAppWorkspace(c); + + const now = Date.now(); + await c.db + .insert(authAccountIndex) + .values({ + id: input.id, + providerId: input.providerId, + accountId: input.accountId, + userId: input.userId, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: authAccountIndex.id, + set: { + providerId: input.providerId, + accountId: input.accountId, + userId: input.userId, + updatedAt: now, + }, + }) + .run(); + return await c.db.select().from(authAccountIndex).where(eq(authAccountIndex.id, input.id)).get(); + }, + + async authDeleteAccountIndex(c: any, input: { id?: string; providerId?: string; accountId?: string }) { + assertAppWorkspace(c); + + if (input.id) { + await c.db.delete(authAccountIndex).where(eq(authAccountIndex.id, input.id)).run(); + return; + } + if (input.providerId && input.accountId) { + await c.db + .delete(authAccountIndex) + .where(and(eq(authAccountIndex.providerId, input.providerId), eq(authAccountIndex.accountId, input.accountId))) + .run(); + } + }, + + async authCreateVerification(c: any, input: { data: Record }) { + assertAppWorkspace(c); + + await c.db + .insert(authVerification) + .values(input.data as any) + .run(); + return await c.db + .select() + .from(authVerification) + .where(eq(authVerification.id, input.data.id as string)) + .get(); + }, + + async authFindOneVerification(c: any, input: { where: any[] }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + return predicate ? await c.db.select().from(authVerification).where(predicate).get() : null; + }, + + async authFindManyVerification(c: any, input: { where?: any[]; limit?: number; sortBy?: any; offset?: number }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + let query = c.db.select().from(authVerification); + if (predicate) { + query = query.where(predicate); + } + if (input.sortBy?.field) { + const column = workspaceAuthColumn(authVerification, input.sortBy.field); + query = query.orderBy(input.sortBy.direction === "asc" ? asc(column) : desc(column)); + } + if (typeof input.limit === "number") { + query = query.limit(input.limit); + } + if (typeof input.offset === "number") { + query = query.offset(input.offset); + } + return await query.all(); + }, + + async authUpdateVerification(c: any, input: { where: any[]; update: Record }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + if (!predicate) { + return null; + } + await c.db + .update(authVerification) + .set(input.update as any) + .where(predicate) + .run(); + return await c.db.select().from(authVerification).where(predicate).get(); + }, + + async authUpdateManyVerification(c: any, input: { where: any[]; update: Record }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + if (!predicate) { + return 0; + } + await c.db + .update(authVerification) + .set(input.update as any) + .where(predicate) + .run(); + const row = await c.db.select({ value: sqlCount() }).from(authVerification).where(predicate).get(); + return row?.value ?? 0; + }, + + async authDeleteVerification(c: any, input: { where: any[] }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + if (!predicate) { + return; + } + await c.db.delete(authVerification).where(predicate).run(); + }, + + async authDeleteManyVerification(c: any, input: { where: any[] }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + if (!predicate) { + return 0; + } + const rows = await c.db.select().from(authVerification).where(predicate).all(); + await c.db.delete(authVerification).where(predicate).run(); + return rows.length; + }, + + async authCountVerification(c: any, input: { where?: any[] }) { + assertAppWorkspace(c); + + const predicate = workspaceAuthWhere(authVerification, input.where); + const row = predicate + ? await c.db.select({ value: sqlCount() }).from(authVerification).where(predicate).get() + : await c.db.select({ value: sqlCount() }).from(authVerification).get(); + return row?.value ?? 0; }, async getAppSnapshot(c: any, input: { sessionId: string }): Promise { @@ -750,20 +985,27 @@ export const workspaceAppActions = { input: { organizationId: string; requireRepoScope?: boolean }, ): Promise<{ accessToken: string; scopes: string[] } | null> { assertAppWorkspace(c); - const rows = await c.db.select().from(appSessions).orderBy(desc(appSessions.updatedAt)).all(); + const auth = getBetterAuthService(); + const rows = await c.db.select().from(authSessionIndex).orderBy(desc(authSessionIndex.updatedAt)).all(); for (const row of rows) { - if (row.activeOrganizationId !== input.organizationId || !row.githubAccessToken) { + const authState = await auth.getAuthState(row.sessionId); + if (authState?.sessionState?.activeOrganizationId !== input.organizationId) { continue; } - const scopes = splitScopes(row.githubScope); - if (input.requireRepoScope !== false && !hasRepoScope(scopes)) { + const token = await auth.getAccessTokenForSession(row.sessionId); + if (!token?.accessToken) { + continue; + } + + const scopes = token.scopes; + if (input.requireRepoScope !== false && scopes.length > 0 && !hasRepoScope(scopes)) { continue; } return { - accessToken: row.githubAccessToken, + accessToken: token.accessToken, scopes, }; } @@ -771,97 +1013,10 @@ export const workspaceAppActions = { return null; }, - async startAppGithubAuth(c: any, input: { sessionId: string }): Promise<{ url: string }> { - assertAppWorkspace(c); - const { appShell } = getActorRuntimeContext(); - const sessionId = await ensureAppSession(c, input.sessionId); - const nonce = randomUUID(); - await updateAppSession(c, sessionId, { - oauthState: nonce, - oauthStateExpiresAt: Date.now() + OAUTH_TTL_MS, - }); - return { - url: appShell.github.buildAuthorizeUrl(encodeOauthState({ sessionId, nonce })), - }; - }, - - async completeAppGithubAuth(c: any, input: { code: string; state: string }): Promise<{ sessionId: string; redirectTo: string }> { - assertAppWorkspace(c); - const { appShell } = getActorRuntimeContext(); - const oauth = decodeOauthState(input.state); - const session = await requireAppSessionRow(c, oauth.sessionId); - if (!session.oauthState || session.oauthState !== oauth.nonce || !session.oauthStateExpiresAt || session.oauthStateExpiresAt < Date.now()) { - throw new Error("GitHub OAuth state is invalid or expired"); - } - - // Clear state before exchangeCode — GitHub codes are single-use and - // duplicate callback requests (from proxy retries or user refresh) - // must fail the state check rather than attempt a second exchange. - // See research/friction/general.mdx 2026-03-13 entry. - await updateAppSession(c, session.id, { - oauthState: null, - oauthStateExpiresAt: null, - }); - - const token = await appShell.github.exchangeCode(input.code); - - // Fast path: store token + user identity and return the redirect - // immediately. The slow org sync (list orgs, list installations, - // sync each workspace) runs in the workflow queue so the HTTP - // response lands before any proxy/infra timeout triggers a retry. - // The frontend already polls when it sees syncStatus === "syncing". - const result = await initGithubSession(c, session.id, token.accessToken, token.scopes); - - // Enqueue the slow org sync to the workflow. fire-and-forget (wait: false) - // because the redirect does not depend on org data — the frontend will - // poll getAppSnapshot until organizations are populated. - const self = selfWorkspace(c); - await self.send( - "workspace.command.syncGithubSession", - { sessionId: session.id, accessToken: token.accessToken }, - { - wait: false, - }, - ); - - return result; - }, - - async bootstrapAppGithubSession(c: any, input: { accessToken: string; sessionId?: string | null }): Promise<{ sessionId: string; redirectTo: string }> { - assertAppWorkspace(c); - if (process.env.NODE_ENV === "production") { - throw new Error("bootstrapAppGithubSession is development-only"); - } - const sessionId = await ensureAppSession(c, input.sessionId ?? null); - return await syncGithubSessionFromToken(c, sessionId, input.accessToken); - }, - - async signOutApp(c: any, input: { sessionId: string }): Promise { - assertAppWorkspace(c); - const sessionId = await ensureAppSession(c, input.sessionId); - await updateAppSession(c, sessionId, { - currentUserId: null, - currentUserName: null, - currentUserEmail: null, - currentUserGithubLogin: null, - currentUserRoleLabel: null, - eligibleOrganizationIdsJson: "[]", - activeOrganizationId: null, - githubAccessToken: null, - githubScope: "", - starterRepoStatus: "pending", - starterRepoStarredAt: null, - starterRepoSkippedAt: null, - oauthState: null, - oauthStateExpiresAt: null, - }); - return await buildAppSnapshot(c, sessionId); - }, - async skipAppStarterRepo(c: any, input: { sessionId: string }): Promise { assertAppWorkspace(c); - await requireSignedInSession(c, input.sessionId); - await updateAppSession(c, input.sessionId, { + const session = await requireSignedInSession(c, input.sessionId); + await getBetterAuthService().upsertUserProfile(session.authUserId, { starterRepoStatus: "skipped", starterRepoSkippedAt: Date.now(), starterRepoStarredAt: null, @@ -877,7 +1032,7 @@ export const workspaceAppActions = { await workspace.starSandboxAgentRepo({ workspaceId: input.organizationId, }); - await updateAppSession(c, input.sessionId, { + await getBetterAuthService().upsertUserProfile(session.authUserId, { starterRepoStatus: "starred", starterRepoStarredAt: Date.now(), starterRepoSkippedAt: null, @@ -889,9 +1044,7 @@ export const workspaceAppActions = { assertAppWorkspace(c); const session = await requireSignedInSession(c, input.sessionId); requireEligibleOrganization(session, input.organizationId); - await updateAppSession(c, input.sessionId, { - activeOrganizationId: input.organizationId, - }); + await getBetterAuthService().setActiveOrganization(input.sessionId, input.organizationId); const workspace = await getOrCreateWorkspace(c, input.organizationId); const organization = await getOrganizationState(workspace); @@ -968,7 +1121,7 @@ export const workspaceAppActions = { const organization = await getOrganizationState(workspace); if (organization.snapshot.kind !== "organization") { return { - url: `${appShell.appUrl}/workspaces/${input.organizationId}?foundrySession=${encodeURIComponent(input.sessionId)}`, + url: `${appShell.appUrl}/workspaces/${input.organizationId}`, }; } return { @@ -987,7 +1140,7 @@ export const workspaceAppActions = { if (input.planId === "free") { await workspace.applyOrganizationFreePlan({ clearSubscription: false }); return { - url: `${appShell.appUrl}/organizations/${input.organizationId}/billing?foundrySession=${encodeURIComponent(input.sessionId)}`, + url: `${appShell.appUrl}/organizations/${input.organizationId}/billing`, }; } @@ -1017,8 +1170,8 @@ export const workspaceAppActions = { planId: input.planId, successUrl: `${appShell.apiUrl}/v1/billing/checkout/complete?organizationId=${encodeURIComponent( input.organizationId, - )}&foundrySession=${encodeURIComponent(input.sessionId)}&session_id={CHECKOUT_SESSION_ID}`, - cancelUrl: `${appShell.appUrl}/organizations/${input.organizationId}/billing?foundrySession=${encodeURIComponent(input.sessionId)}`, + )}&session_id={CHECKOUT_SESSION_ID}`, + cancelUrl: `${appShell.appUrl}/organizations/${input.organizationId}/billing`, }) .then((checkout) => checkout.url), }; @@ -1048,7 +1201,7 @@ export const workspaceAppActions = { } return { - redirectTo: `${appShell.appUrl}/organizations/${input.organizationId}/billing?foundrySession=${encodeURIComponent(input.sessionId)}`, + redirectTo: `${appShell.appUrl}/organizations/${input.organizationId}/billing`, }; }, @@ -1064,7 +1217,7 @@ export const workspaceAppActions = { } const portal = await appShell.stripe.createPortalSession({ customerId: organization.stripeCustomerId, - returnUrl: `${appShell.appUrl}/organizations/${input.organizationId}/billing?foundrySession=${encodeURIComponent(input.sessionId)}`, + returnUrl: `${appShell.appUrl}/organizations/${input.organizationId}/billing`, }); return { url: portal.url }; }, diff --git a/foundry/packages/backend/src/actors/workspace/db/migrations.ts b/foundry/packages/backend/src/actors/workspace/db/migrations.ts index 6832f80..a0f2f74 100644 --- a/foundry/packages/backend/src/actors/workspace/db/migrations.ts +++ b/foundry/packages/backend/src/actors/workspace/db/migrations.ts @@ -10,6 +10,12 @@ const journal = { tag: "0000_melted_viper", breakpoints: true, }, + { + idx: 1, + when: 1773638400000, + tag: "0001_auth_index_tables", + breakpoints: true, + }, ], } as const; @@ -113,6 +119,37 @@ CREATE TABLE \`task_lookup\` ( \`task_id\` text PRIMARY KEY NOT NULL, \`repo_id\` text NOT NULL ); +`, + m0001: `CREATE TABLE IF NOT EXISTS \`auth_session_index\` ( + \`session_id\` text PRIMARY KEY NOT NULL, + \`session_token\` text NOT NULL, + \`user_id\` text NOT NULL, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS \`auth_email_index\` ( + \`email\` text PRIMARY KEY NOT NULL, + \`user_id\` text NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS \`auth_account_index\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`provider_id\` text NOT NULL, + \`account_id\` text NOT NULL, + \`user_id\` text NOT NULL, + \`updated_at\` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS \`auth_verification\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`identifier\` text NOT NULL, + \`value\` text NOT NULL, + \`expires_at\` integer NOT NULL, + \`created_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL +); `, } as const, }; diff --git a/foundry/packages/backend/src/actors/workspace/db/schema.ts b/foundry/packages/backend/src/actors/workspace/db/schema.ts index 5f8cf66..ca40a88 100644 --- a/foundry/packages/backend/src/actors/workspace/db/schema.ts +++ b/foundry/packages/backend/src/actors/workspace/db/schema.ts @@ -74,23 +74,33 @@ export const invoices = sqliteTable("invoices", { createdAt: integer("created_at").notNull(), }); -export const appSessions = sqliteTable("app_sessions", { +export const authSessionIndex = sqliteTable("auth_session_index", { + sessionId: text("session_id").notNull().primaryKey(), + sessionToken: text("session_token").notNull(), + userId: text("user_id").notNull(), + createdAt: integer("created_at").notNull(), + updatedAt: integer("updated_at").notNull(), +}); + +export const authEmailIndex = sqliteTable("auth_email_index", { + email: text("email").notNull().primaryKey(), + userId: text("user_id").notNull(), + updatedAt: integer("updated_at").notNull(), +}); + +export const authAccountIndex = sqliteTable("auth_account_index", { id: text("id").notNull().primaryKey(), - currentUserId: text("current_user_id"), - currentUserName: text("current_user_name"), - currentUserEmail: text("current_user_email"), - currentUserGithubLogin: text("current_user_github_login"), - currentUserRoleLabel: text("current_user_role_label"), - // Structured as a JSON array of eligible organization ids for the session. - eligibleOrganizationIdsJson: text("eligible_organization_ids_json").notNull(), - activeOrganizationId: text("active_organization_id"), - githubAccessToken: text("github_access_token"), - githubScope: text("github_scope").notNull(), - starterRepoStatus: text("starter_repo_status").notNull(), - starterRepoStarredAt: integer("starter_repo_starred_at"), - starterRepoSkippedAt: integer("starter_repo_skipped_at"), - oauthState: text("oauth_state"), - oauthStateExpiresAt: integer("oauth_state_expires_at"), + providerId: text("provider_id").notNull(), + accountId: text("account_id").notNull(), + userId: text("user_id").notNull(), + updatedAt: integer("updated_at").notNull(), +}); + +export const authVerification = sqliteTable("auth_verification", { + id: text("id").notNull().primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: integer("expires_at").notNull(), createdAt: integer("created_at").notNull(), updatedAt: integer("updated_at").notNull(), }); diff --git a/foundry/packages/backend/src/index.ts b/foundry/packages/backend/src/index.ts index 17acb4a..9875c0a 100644 --- a/foundry/packages/backend/src/index.ts +++ b/foundry/packages/backend/src/index.ts @@ -10,6 +10,7 @@ import { createDefaultDriver } from "./driver.js"; import { createProviderRegistry } from "./providers/index.js"; import { createClient } from "rivetkit/client"; import type { FoundryBillingPlanId } from "@sandbox-agent/foundry-shared"; +import { initBetterAuthService } from "./services/better-auth.js"; import { createDefaultAppShellServices } from "./services/app-shell-runtime.js"; import { APP_SHELL_WORKSPACE_ID } from "./actors/workspace/app-shell.js"; import { logger } from "./logging.js"; @@ -39,33 +40,15 @@ interface AppWorkspaceLogContext { xRealIp?: string; } +function stripTrailingSlash(value: string): string { + return value.replace(/\/$/, ""); +} + function isRivetRequest(request: Request): boolean { const { pathname } = new URL(request.url); return pathname === "/v1/rivet" || pathname.startsWith("/v1/rivet/"); } -function isRetryableAppActorError(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error); - return message.includes("Actor not ready") || message.includes("socket connection was closed unexpectedly"); -} - -async function withRetries(run: () => Promise, attempts = 20, delayMs = 250): Promise { - let lastError: unknown; - for (let attempt = 1; attempt <= attempts; attempt += 1) { - try { - return await run(); - } catch (error) { - lastError = error; - if (!isRetryableAppActorError(error) || attempt === attempts) { - throw error; - } - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } - } - - throw lastError instanceof Error ? lastError : new Error(String(lastError)); -} - export async function startBackend(options: BackendStartOptions = {}): Promise { // sandbox-agent agent plugins vary on which env var they read for OpenAI/Codex auth. // Normalize to keep local dev + docker-compose simple. @@ -94,11 +77,16 @@ export async function startBackend(options: BackendStartOptions = {}): Promise ({ cfConnectingIp: c.req.header("cf-connecting-ip") ?? undefined, @@ -131,29 +119,18 @@ export async function startBackend(options: BackendStartOptions = {}): Promise origin ?? "*", - credentials: true, - allowHeaders, - allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - exposeHeaders, - }), - ); - app.use( - "/v1", - cors({ - origin: (origin) => origin ?? "*", - credentials: true, - allowHeaders, - allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - exposeHeaders, - }), - ); + const exposeHeaders = ["Content-Type", "x-rivet-ray-id"]; + const allowedOrigins = new Set([stripTrailingSlash(appShellServices.appUrl), stripTrailingSlash(appShellServices.apiUrl)]); + const corsConfig = { + origin: (origin: string) => (allowedOrigins.has(origin) ? origin : null) as string | undefined | null, + credentials: true, + allowHeaders, + allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + exposeHeaders, + }; + app.use("/v1/*", cors(corsConfig)); + app.use("/v1", cors(corsConfig)); app.use("*", async (c, next) => { const requestId = c.req.header("x-request-id")?.trim() || randomUUID(); const start = performance.now(); @@ -190,6 +167,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { @@ -197,12 +177,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise - await actorClient.workspace.getOrCreate(workspaceKey(APP_SHELL_WORKSPACE_ID), { - createWithInput: APP_SHELL_WORKSPACE_ID, - }), - ); + const handle = await actorClient.workspace.getOrCreate(workspaceKey(APP_SHELL_WORKSPACE_ID), { + createWithInput: APP_SHELL_WORKSPACE_ID, + }); cachedAppWorkspace = handle; logger.info( { @@ -253,68 +230,70 @@ export async function startBackend(options: BackendStartOptions = {}): Promise => { - const requested = c.req.header("x-foundry-session"); - const { sessionId } = await appWorkspaceAction( - "ensureAppSession", - async (workspace) => await workspace.ensureAppSession(requested && requested.trim().length > 0 ? { requestedSessionId: requested } : {}), - requestLogContext(c), - ); - c.header("x-foundry-session", sessionId); - return sessionId; + const resolveSessionId = async (c: any): Promise => { + const session = await betterAuth.resolveSession(c.req.raw.headers); + return session?.session?.id ?? null; }; app.get("/v1/app/snapshot", async (c) => { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.json({ + auth: { status: "signed_out", currentUserId: null }, + activeOrganizationId: null, + onboarding: { + starterRepo: { + repoFullName: "rivet-dev/sandbox-agent", + repoUrl: "https://github.com/rivet-dev/sandbox-agent", + status: "pending", + starredAt: null, + skippedAt: null, + }, + }, + users: [], + organizations: [], + }); + } return c.json( await appWorkspaceAction("getAppSnapshot", async (workspace) => await workspace.getAppSnapshot({ sessionId }), requestLogContext(c, sessionId)), ); }); - app.get("/v1/auth/github/start", async (c) => { - const sessionId = await resolveSessionId(c); - const result = await appWorkspaceAction( - "startAppGithubAuth", - async (workspace) => await workspace.startAppGithubAuth({ sessionId }), - requestLogContext(c, sessionId), - ); - return Response.redirect(result.url, 302); + app.all("/v1/auth/*", async (c) => { + return await betterAuth.auth.handler(c.req.raw); }); - const handleGithubAuthCallback = async (c: any) => { - // TEMPORARY: dump all request headers to diagnose duplicate callback requests - // (Railway nginx proxy_next_upstream? Cloudflare retry? browser?) - // Remove once root cause is identified. - const allHeaders: Record = {}; - c.req.raw.headers.forEach((value: string, key: string) => { - allHeaders[key] = value; - }); - logger.info({ headers: allHeaders, url: c.req.url }, "github_callback_headers"); - - const code = c.req.query("code"); - const state = c.req.query("state"); - if (!code || !state) { - return c.text("Missing GitHub OAuth callback parameters", 400); - } - const result = await appWorkspaceAction( - "completeAppGithubAuth", - async (workspace) => await workspace.completeAppGithubAuth({ code, state }), - requestLogContext(c), - ); - c.header("x-foundry-session", result.sessionId); - return Response.redirect(result.redirectTo, 302); - }; - - app.get("/v1/auth/github/callback", handleGithubAuthCallback); - app.get("/api/auth/callback/github", handleGithubAuthCallback); - app.post("/v1/app/sign-out", async (c) => { const sessionId = await resolveSessionId(c); - return c.json(await appWorkspaceAction("signOutApp", async (workspace) => await workspace.signOutApp({ sessionId }), requestLogContext(c, sessionId))); + if (sessionId) { + const signOutResponse = await betterAuth.signOut(c.req.raw.headers); + const setCookie = signOutResponse.headers.get("set-cookie"); + if (setCookie) { + c.header("set-cookie", setCookie); + } + } + return c.json({ + auth: { status: "signed_out", currentUserId: null }, + activeOrganizationId: null, + onboarding: { + starterRepo: { + repoFullName: "rivet-dev/sandbox-agent", + repoUrl: "https://github.com/rivet-dev/sandbox-agent", + status: "pending", + starredAt: null, + skippedAt: null, + }, + }, + users: [], + organizations: [], + }); }); app.post("/v1/app/onboarding/starter-repo/skip", async (c) => { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await appWorkspaceAction("skipAppStarterRepo", async (workspace) => await workspace.skipAppStarterRepo({ sessionId }), requestLogContext(c, sessionId)), ); @@ -322,6 +301,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await appWorkspaceAction( "starAppStarterRepo", @@ -337,6 +319,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await appWorkspaceAction( "selectAppOrganization", @@ -352,6 +337,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } const body = await c.req.json(); return c.json( await appWorkspaceAction( @@ -371,6 +359,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await appWorkspaceAction( "triggerAppRepoImport", @@ -386,6 +377,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await appWorkspaceAction( "beginAppGithubInstall", @@ -401,6 +395,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } const body = await c.req.json().catch(() => ({})); const planId = body?.planId === "free" || body?.planId === "team" ? (body.planId as FoundryBillingPlanId) : "team"; return c.json( @@ -414,11 +411,14 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const organizationId = c.req.query("organizationId"); - const sessionId = c.req.query("foundrySession"); const checkoutSessionId = c.req.query("session_id"); - if (!organizationId || !sessionId || !checkoutSessionId) { + if (!organizationId || !checkoutSessionId) { return c.text("Missing Stripe checkout completion parameters", 400); } + const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } const result = await (await appWorkspace(requestLogContext(c, sessionId))).finalizeAppCheckoutSession({ organizationId, sessionId, @@ -429,6 +429,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await (await appWorkspace(requestLogContext(c, sessionId))).createAppBillingPortalSession({ sessionId, @@ -439,6 +442,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await (await appWorkspace(requestLogContext(c, sessionId))).cancelAppScheduledRenewal({ sessionId, @@ -449,6 +455,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await (await appWorkspace(requestLogContext(c, sessionId))).resumeAppSubscription({ sessionId, @@ -459,6 +468,9 @@ export async function startBackend(options: BackendStartOptions = {}): Promise { const sessionId = await resolveSessionId(c); + if (!sessionId) { + return c.text("Unauthorized", 401); + } return c.json( await (await appWorkspace(requestLogContext(c, sessionId))).recordAppSeatUsage({ sessionId, diff --git a/foundry/packages/backend/src/services/better-auth.ts b/foundry/packages/backend/src/services/better-auth.ts new file mode 100644 index 0000000..325ea59 --- /dev/null +++ b/foundry/packages/backend/src/services/better-auth.ts @@ -0,0 +1,533 @@ +import { betterAuth } from "better-auth"; +import { createAdapterFactory } from "better-auth/adapters"; +import { APP_SHELL_WORKSPACE_ID } from "../actors/workspace/app-shell.js"; +import { authUserKey, workspaceKey } from "../actors/keys.js"; +import { logger } from "../logging.js"; + +const AUTH_BASE_PATH = "/v1/auth"; +const SESSION_COOKIE = "better-auth.session_token"; + +let betterAuthService: BetterAuthService | null = null; + +function requireEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) { + throw new Error(`${name} is required`); + } + return value; +} + +function stripTrailingSlash(value: string): string { + return value.replace(/\/$/, ""); +} + +function buildCookieHeaders(sessionToken: string): Headers { + return new Headers({ + cookie: `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}`, + }); +} + +async function readJsonSafe(response: Response): Promise { + const text = await response.text(); + if (!text) { + return null; + } + try { + return JSON.parse(text); + } catch { + return text; + } +} + +async function callAuthEndpoint(auth: any, url: string, init?: RequestInit): Promise { + return await auth.handler(new Request(url, init)); +} + +function resolveRouteUserId(workspace: any, resolved: any): string | null { + if (!resolved) { + return null; + } + if (typeof resolved === "string") { + return resolved; + } + if (typeof resolved.userId === "string" && resolved.userId.length > 0) { + return resolved.userId; + } + if (typeof resolved.id === "string" && resolved.id.length > 0) { + return resolved.id; + } + return null; +} + +export interface BetterAuthService { + auth: any; + resolveSession(headers: Headers): Promise<{ session: any; user: any } | null>; + signOut(headers: Headers): Promise; + getAuthState(sessionId: string): Promise; + upsertUserProfile(userId: string, patch: Record): Promise; + setActiveOrganization(sessionId: string, activeOrganizationId: string | null): Promise; + getAccessTokenForSession(sessionId: string): Promise<{ accessToken: string; scopes: string[] } | null>; +} + +export function initBetterAuthService(actorClient: any, options: { apiUrl: string; appUrl: string }): BetterAuthService { + if (betterAuthService) { + return betterAuthService; + } + + // getOrCreate is intentional here: the adapter runs during Better Auth callbacks + // which can fire before any explicit create path. The app workspace and auth user + // actors must exist by the time the adapter needs them. + const appWorkspace = () => + actorClient.workspace.getOrCreate(workspaceKey(APP_SHELL_WORKSPACE_ID), { + createWithInput: APP_SHELL_WORKSPACE_ID, + }); + + // getOrCreate is intentional: Better Auth creates user records during OAuth + // callbacks, so the auth-user actor must be lazily provisioned on first access. + const getAuthUser = async (userId: string) => + await actorClient.authUser.getOrCreate(authUserKey(userId), { + createWithInput: { userId }, + }); + + const adapter = createAdapterFactory({ + config: { + adapterId: "rivetkit-actor", + adapterName: "RivetKit Actor Adapter", + supportsBooleans: false, + supportsDates: false, + supportsJSON: false, + }, + adapter: ({ transformInput, transformOutput, transformWhereClause }) => { + const resolveUserIdForQuery = async (model: string, where?: any[], data?: Record): Promise => { + const clauses = where ?? []; + const direct = (field: string) => clauses.find((entry) => entry.field === field)?.value; + + if (model === "user") { + const fromId = direct("id") ?? data?.id; + if (typeof fromId === "string" && fromId.length > 0) { + return fromId; + } + const email = direct("email"); + if (typeof email === "string" && email.length > 0) { + const workspace = await appWorkspace(); + const resolved = await workspace.authFindEmailIndex({ email: email.toLowerCase() }); + return resolveRouteUserId(workspace, resolved); + } + return null; + } + + if (model === "session") { + const fromUserId = direct("userId") ?? data?.userId; + if (typeof fromUserId === "string" && fromUserId.length > 0) { + return fromUserId; + } + const sessionId = direct("id") ?? data?.id; + const sessionToken = direct("token") ?? data?.token; + if (typeof sessionId === "string" || typeof sessionToken === "string") { + const workspace = await appWorkspace(); + const resolved = await workspace.authFindSessionIndex({ + ...(typeof sessionId === "string" ? { sessionId } : {}), + ...(typeof sessionToken === "string" ? { sessionToken } : {}), + }); + return resolveRouteUserId(workspace, resolved); + } + return null; + } + + if (model === "account") { + const fromUserId = direct("userId") ?? data?.userId; + if (typeof fromUserId === "string" && fromUserId.length > 0) { + return fromUserId; + } + const accountRecordId = direct("id") ?? data?.id; + const providerId = direct("providerId") ?? data?.providerId; + const accountId = direct("accountId") ?? data?.accountId; + const workspace = await appWorkspace(); + if (typeof accountRecordId === "string" && accountRecordId.length > 0) { + const resolved = await workspace.authFindAccountIndex({ id: accountRecordId }); + return resolveRouteUserId(workspace, resolved); + } + if (typeof providerId === "string" && providerId.length > 0 && typeof accountId === "string" && accountId.length > 0) { + const resolved = await workspace.authFindAccountIndex({ providerId, accountId }); + return resolveRouteUserId(workspace, resolved); + } + return null; + } + + return null; + }; + + const ensureWorkspaceVerification = async (method: string, payload: Record) => { + const workspace = await appWorkspace(); + return await workspace[method](payload); + }; + + return { + options: { + useDatabaseGeneratedIds: false, + }, + + create: async ({ model, data }) => { + const transformed = await transformInput(data, model, "create", true); + if (model === "verification") { + return await ensureWorkspaceVerification("authCreateVerification", { data: transformed }); + } + + const userId = await resolveUserIdForQuery(model, undefined, transformed); + if (!userId) { + throw new Error(`Unable to resolve auth actor for create(${model})`); + } + + const userActor = await getAuthUser(userId); + const created = await userActor.createAuthRecord({ model, data: transformed }); + const workspace = await appWorkspace(); + + if (model === "user" && typeof transformed.email === "string" && transformed.email.length > 0) { + await workspace.authUpsertEmailIndex({ + email: transformed.email.toLowerCase(), + userId, + }); + } + + if (model === "session") { + await workspace.authUpsertSessionIndex({ + sessionId: String(created.id), + sessionToken: String(created.token), + userId, + }); + } + + if (model === "account") { + await workspace.authUpsertAccountIndex({ + id: String(created.id), + providerId: String(created.providerId), + accountId: String(created.accountId), + userId, + }); + } + + return (await transformOutput(created, model)) as any; + }, + + findOne: async ({ model, where, join }) => { + const transformedWhere = transformWhereClause({ model, where, action: "findOne" }); + if (model === "verification") { + return await ensureWorkspaceVerification("authFindOneVerification", { where: transformedWhere, join }); + } + + const userId = await resolveUserIdForQuery(model, transformedWhere); + if (!userId) { + return null; + } + + const userActor = await getAuthUser(userId); + const found = await userActor.findOneAuthRecord({ model, where: transformedWhere, join }); + return found ? ((await transformOutput(found, model, undefined, join)) as any) : null; + }, + + findMany: async ({ model, where, limit, sortBy, offset, join }) => { + const transformedWhere = transformWhereClause({ model, where, action: "findMany" }); + if (model === "verification") { + return await ensureWorkspaceVerification("authFindManyVerification", { + where: transformedWhere, + limit, + sortBy, + offset, + join, + }); + } + + if (model === "session") { + const tokenClause = transformedWhere?.find((entry: any) => entry.field === "token" && entry.operator === "in"); + if (tokenClause && Array.isArray(tokenClause.value)) { + const workspace = await appWorkspace(); + const resolved = await Promise.all( + (tokenClause.value as string[]).map(async (sessionToken: string) => ({ + sessionToken, + route: await workspace.authFindSessionIndex({ sessionToken }), + })), + ); + const byUser = new Map(); + for (const item of resolved) { + if (!item.route?.userId) { + continue; + } + const tokens = byUser.get(item.route.userId) ?? []; + tokens.push(item.sessionToken); + byUser.set(item.route.userId, tokens); + } + + const rows = []; + for (const [userId, tokens] of byUser) { + const userActor = await getAuthUser(userId); + const scopedWhere = transformedWhere.map((entry: any) => + entry.field === "token" && entry.operator === "in" ? { ...entry, value: tokens } : entry, + ); + const found = await userActor.findManyAuthRecords({ model, where: scopedWhere, limit, sortBy, offset, join }); + rows.push(...found); + } + return await Promise.all(rows.map(async (row: any) => await transformOutput(row, model, undefined, join))); + } + } + + const userId = await resolveUserIdForQuery(model, transformedWhere); + if (!userId) { + return []; + } + + const userActor = await getAuthUser(userId); + const found = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit, sortBy, offset, join }); + return await Promise.all(found.map(async (row: any) => await transformOutput(row, model, undefined, join))); + }, + + update: async ({ model, where, update }) => { + const transformedWhere = transformWhereClause({ model, where, action: "update" }); + const transformedUpdate = (await transformInput(update as Record, model, "update", true)) as Record; + if (model === "verification") { + return await ensureWorkspaceVerification("authUpdateVerification", { where: transformedWhere, update: transformedUpdate }); + } + + const userId = await resolveUserIdForQuery(model, transformedWhere, transformedUpdate); + if (!userId) { + return null; + } + + const userActor = await getAuthUser(userId); + const before = + model === "user" + ? await userActor.findOneAuthRecord({ model, where: transformedWhere }) + : model === "account" + ? await userActor.findOneAuthRecord({ model, where: transformedWhere }) + : model === "session" + ? await userActor.findOneAuthRecord({ model, where: transformedWhere }) + : null; + const updated = await userActor.updateAuthRecord({ model, where: transformedWhere, update: transformedUpdate }); + const workspace = await appWorkspace(); + + if (model === "user" && updated) { + if (before?.email && before.email !== updated.email) { + await workspace.authDeleteEmailIndex({ email: before.email.toLowerCase() }); + } + if (updated.email) { + await workspace.authUpsertEmailIndex({ email: updated.email.toLowerCase(), userId }); + } + } + + if (model === "session" && updated) { + await workspace.authUpsertSessionIndex({ + sessionId: String(updated.id), + sessionToken: String(updated.token), + userId, + }); + } + + if (model === "account" && updated) { + await workspace.authUpsertAccountIndex({ + id: String(updated.id), + providerId: String(updated.providerId), + accountId: String(updated.accountId), + userId, + }); + } + + return updated ? ((await transformOutput(updated, model)) as any) : null; + }, + + updateMany: async ({ model, where, update }) => { + const transformedWhere = transformWhereClause({ model, where, action: "updateMany" }); + const transformedUpdate = (await transformInput(update as Record, model, "update", true)) as Record; + if (model === "verification") { + return await ensureWorkspaceVerification("authUpdateManyVerification", { where: transformedWhere, update: transformedUpdate }); + } + + const userId = await resolveUserIdForQuery(model, transformedWhere, transformedUpdate); + if (!userId) { + return 0; + } + + const userActor = await getAuthUser(userId); + return await userActor.updateManyAuthRecords({ model, where: transformedWhere, update: transformedUpdate }); + }, + + delete: async ({ model, where }) => { + const transformedWhere = transformWhereClause({ model, where, action: "delete" }); + if (model === "verification") { + await ensureWorkspaceVerification("authDeleteVerification", { where: transformedWhere }); + return; + } + + const userId = await resolveUserIdForQuery(model, transformedWhere); + if (!userId) { + return; + } + + const userActor = await getAuthUser(userId); + const workspace = await appWorkspace(); + const before = await userActor.findOneAuthRecord({ model, where: transformedWhere }); + await userActor.deleteAuthRecord({ model, where: transformedWhere }); + + if (model === "session" && before) { + await workspace.authDeleteSessionIndex({ + sessionId: before.id, + sessionToken: before.token, + }); + } + + if (model === "account" && before) { + await workspace.authDeleteAccountIndex({ + id: before.id, + providerId: before.providerId, + accountId: before.accountId, + }); + } + + if (model === "user" && before?.email) { + await workspace.authDeleteEmailIndex({ email: before.email.toLowerCase() }); + } + }, + + deleteMany: async ({ model, where }) => { + const transformedWhere = transformWhereClause({ model, where, action: "deleteMany" }); + if (model === "verification") { + return await ensureWorkspaceVerification("authDeleteManyVerification", { where: transformedWhere }); + } + + if (model === "session") { + const userId = await resolveUserIdForQuery(model, transformedWhere); + if (!userId) { + return 0; + } + const userActor = await getAuthUser(userId); + const workspace = await appWorkspace(); + const sessions = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit: 5000 }); + const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere }); + for (const session of sessions) { + await workspace.authDeleteSessionIndex({ + sessionId: session.id, + sessionToken: session.token, + }); + } + return deleted; + } + + const userId = await resolveUserIdForQuery(model, transformedWhere); + if (!userId) { + return 0; + } + + const userActor = await getAuthUser(userId); + const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere }); + return deleted; + }, + + count: async ({ model, where }) => { + const transformedWhere = transformWhereClause({ model, where, action: "count" }); + if (model === "verification") { + return await ensureWorkspaceVerification("authCountVerification", { where: transformedWhere }); + } + + const userId = await resolveUserIdForQuery(model, transformedWhere); + if (!userId) { + return 0; + } + + const userActor = await getAuthUser(userId); + return await userActor.countAuthRecords({ model, where: transformedWhere }); + }, + }; + }, + }); + + const auth = betterAuth({ + baseURL: stripTrailingSlash(process.env.BETTER_AUTH_URL ?? options.apiUrl), + basePath: AUTH_BASE_PATH, + secret: requireEnv("BETTER_AUTH_SECRET"), + database: adapter, + trustedOrigins: [stripTrailingSlash(options.appUrl), stripTrailingSlash(options.apiUrl)], + session: { + cookieCache: { + enabled: true, + maxAge: 5 * 60, + strategy: "compact", + }, + }, + socialProviders: { + github: { + clientId: requireEnv("GITHUB_CLIENT_ID"), + clientSecret: requireEnv("GITHUB_CLIENT_SECRET"), + scope: ["read:org", "repo"], + redirectURI: process.env.GITHUB_REDIRECT_URI || undefined, + }, + }, + }); + + betterAuthService = { + auth, + + async resolveSession(headers: Headers) { + return (await auth.api.getSession({ headers })) ?? null; + }, + + async signOut(headers: Headers) { + return await callAuthEndpoint(auth, `${stripTrailingSlash(process.env.BETTER_AUTH_URL ?? options.apiUrl)}${AUTH_BASE_PATH}/sign-out`, { + method: "POST", + headers, + }); + }, + + async getAuthState(sessionId: string) { + const workspace = await appWorkspace(); + const route = await workspace.authFindSessionIndex({ sessionId }); + if (!route?.userId) { + return null; + } + const userActor = await getAuthUser(route.userId); + return await userActor.getAppAuthState({ sessionId }); + }, + + async upsertUserProfile(userId: string, patch: Record) { + const userActor = await getAuthUser(userId); + return await userActor.upsertUserProfile({ userId, patch }); + }, + + async setActiveOrganization(sessionId: string, activeOrganizationId: string | null) { + const authState = await this.getAuthState(sessionId); + if (!authState?.user?.id) { + throw new Error(`Unknown auth session ${sessionId}`); + } + const userActor = await getAuthUser(authState.user.id); + return await userActor.upsertSessionState({ sessionId, activeOrganizationId }); + }, + + async getAccessTokenForSession(sessionId: string) { + // Read the GitHub access token directly from the account record stored in the + // auth user actor. Better Auth's internal /get-access-token endpoint requires + // session middleware resolution which fails for server-side internal calls (403), + // so we bypass it and read the stored token from our adapter layer directly. + const authState = await this.getAuthState(sessionId); + if (!authState?.user?.id || !authState?.accounts) { + return null; + } + + const githubAccount = authState.accounts.find((account: any) => account.providerId === "github"); + if (!githubAccount?.accessToken) { + logger.warn({ sessionId, userId: authState.user.id }, "get_access_token_no_github_account"); + return null; + } + + return { + accessToken: githubAccount.accessToken, + scopes: githubAccount.scope ? githubAccount.scope.split(/[, ]+/) : [], + }; + }, + }; + + return betterAuthService; +} + +export function getBetterAuthService(): BetterAuthService { + if (!betterAuthService) { + throw new Error("BetterAuth service is not initialized"); + } + return betterAuthService; +} diff --git a/foundry/packages/client/src/backend-client.ts b/foundry/packages/client/src/backend-client.ts index d9ffa06..19c119f 100644 --- a/foundry/packages/client/src/backend-client.ts +++ b/foundry/packages/client/src/backend-client.ts @@ -38,14 +38,6 @@ import { sandboxInstanceKey, workspaceKey } from "./keys.js"; export type TaskAction = "push" | "sync" | "merge" | "archive" | "kill"; -type RivetMetadataResponse = { - runtime?: string; - actorNames?: Record; - clientEndpoint?: string; - clientNamespace?: string; - clientToken?: string; -}; - export interface SandboxSessionRecord { id: string; agent: string; @@ -138,16 +130,9 @@ export interface BackendClientOptions { mode?: "remote" | "mock"; } -export interface BackendMetadata { - runtime?: string; - actorNames?: Record; - clientEndpoint?: string; - clientNamespace?: string; - clientToken?: string; -} - export interface BackendClient { getAppSnapshot(): Promise; + subscribeApp(listener: () => void): () => void; signInWithGithub(): Promise; signOutApp(): Promise; skipAppStarterRepo(): Promise; @@ -295,118 +280,6 @@ function deriveBackendEndpoints(endpoint: string): { appEndpoint: string; rivetE }; } -function isLoopbackHost(hostname: string): boolean { - const h = hostname.toLowerCase(); - return h === "127.0.0.1" || h === "localhost" || h === "0.0.0.0" || h === "::1"; -} - -function rewriteLoopbackClientEndpoint(clientEndpoint: string, fallbackOrigin: string): string { - const clientUrl = new URL(clientEndpoint); - if (!isLoopbackHost(clientUrl.hostname)) { - return clientUrl.toString().replace(/\/$/, ""); - } - - const originUrl = new URL(fallbackOrigin); - // Keep the manager port from clientEndpoint; only rewrite host/protocol to match the origin. - clientUrl.hostname = originUrl.hostname; - clientUrl.protocol = originUrl.protocol; - return clientUrl.toString().replace(/\/$/, ""); -} - -async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - try { - const res = await fetch(url, { signal: controller.signal }); - if (!res.ok) { - throw new Error(`request failed: ${res.status} ${res.statusText}`); - } - return (await res.json()) as unknown; - } finally { - clearTimeout(timeout); - } -} - -async function fetchMetadataWithRetry( - endpoint: string, - namespace: string | undefined, - opts: { timeoutMs: number; requestTimeoutMs: number }, -): Promise { - const base = new URL(endpoint); - base.pathname = base.pathname.replace(/\/$/, "") + "/metadata"; - if (namespace) { - base.searchParams.set("namespace", namespace); - } - - const start = Date.now(); - let delayMs = 250; - // Keep this bounded: callers (UI/CLI) should not hang forever if the backend is down. - for (;;) { - try { - const json = await fetchJsonWithTimeout(base.toString(), opts.requestTimeoutMs); - if (!json || typeof json !== "object") return {}; - const data = json as Record; - return { - runtime: typeof data.runtime === "string" ? data.runtime : undefined, - actorNames: data.actorNames && typeof data.actorNames === "object" ? (data.actorNames as Record) : undefined, - clientEndpoint: typeof data.clientEndpoint === "string" ? data.clientEndpoint : undefined, - clientNamespace: typeof data.clientNamespace === "string" ? data.clientNamespace : undefined, - clientToken: typeof data.clientToken === "string" ? data.clientToken : undefined, - }; - } catch (err) { - if (Date.now() - start > opts.timeoutMs) { - throw err; - } - await new Promise((r) => setTimeout(r, delayMs)); - delayMs = Math.min(delayMs * 2, 2_000); - } - } -} - -export async function readBackendMetadata(input: { endpoint: string; namespace?: string; timeoutMs?: number }): Promise { - const base = new URL(input.endpoint); - base.pathname = base.pathname.replace(/\/$/, "") + "/metadata"; - if (input.namespace) { - base.searchParams.set("namespace", input.namespace); - } - - const json = await fetchJsonWithTimeout(base.toString(), input.timeoutMs ?? 4_000); - if (!json || typeof json !== "object") { - return {}; - } - const data = json as Record; - return { - runtime: typeof data.runtime === "string" ? data.runtime : undefined, - actorNames: data.actorNames && typeof data.actorNames === "object" ? (data.actorNames as Record) : undefined, - clientEndpoint: typeof data.clientEndpoint === "string" ? data.clientEndpoint : undefined, - clientNamespace: typeof data.clientNamespace === "string" ? data.clientNamespace : undefined, - clientToken: typeof data.clientToken === "string" ? data.clientToken : undefined, - }; -} - -export async function checkBackendHealth(input: { endpoint: string; namespace?: string; timeoutMs?: number }): Promise { - try { - const metadata = await readBackendMetadata(input); - return metadata.runtime === "rivetkit" && Boolean(metadata.actorNames); - } catch { - return false; - } -} - -async function probeMetadataEndpoint(endpoint: string, namespace: string | undefined, timeoutMs: number): Promise { - try { - const base = new URL(endpoint); - base.pathname = base.pathname.replace(/\/$/, "") + "/metadata"; - if (namespace) { - base.searchParams.set("namespace", namespace); - } - await fetchJsonWithTimeout(base.toString(), timeoutMs); - return true; - } catch { - return false; - } -} - export function createBackendClient(options: BackendClientOptions): BackendClient { if (options.mode === "mock") { return createMockBackendClient(options.defaultWorkspaceId); @@ -415,8 +288,7 @@ export function createBackendClient(options: BackendClientOptions): BackendClien const endpoints = deriveBackendEndpoints(options.endpoint); const rivetApiEndpoint = endpoints.rivetEndpoint; const appApiEndpoint = endpoints.appEndpoint; - let clientPromise: Promise | null = null; - let appSessionId = typeof window !== "undefined" ? window.localStorage.getItem("sandbox-agent-foundry:remote-app-session") : null; + const client = createClient({ endpoint: rivetApiEndpoint }) as unknown as RivetClient; const workbenchSubscriptions = new Map< string, { @@ -431,34 +303,13 @@ export function createBackendClient(options: BackendClientOptions): BackendClien disposeConnPromise: Promise<(() => Promise) | null> | null; } >(); - - const persistAppSessionId = (nextSessionId: string | null): void => { - appSessionId = nextSessionId; - if (typeof window === "undefined") { - return; - } - if (nextSessionId) { - window.localStorage.setItem("sandbox-agent-foundry:remote-app-session", nextSessionId); - } else { - window.localStorage.removeItem("sandbox-agent-foundry:remote-app-session"); - } + const appSubscriptions = { + listeners: new Set<() => void>(), + disposeConnPromise: null as Promise<(() => Promise) | null> | null, }; - if (typeof window !== "undefined") { - const url = new URL(window.location.href); - const sessionFromUrl = url.searchParams.get("foundrySession"); - if (sessionFromUrl) { - persistAppSessionId(sessionFromUrl); - url.searchParams.delete("foundrySession"); - window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`); - } - } - const appRequest = async (path: string, init?: RequestInit): Promise => { const headers = new Headers(init?.headers); - if (appSessionId) { - headers.set("x-foundry-session", appSessionId); - } if (init?.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } @@ -468,10 +319,6 @@ export function createBackendClient(options: BackendClientOptions): BackendClien headers, credentials: "include", }); - const nextSessionId = res.headers.get("x-foundry-session"); - if (nextSessionId) { - persistAppSessionId(nextSessionId); - } if (!res.ok) { throw new Error(`app request failed: ${res.status} ${res.statusText}`); } @@ -485,51 +332,12 @@ export function createBackendClient(options: BackendClientOptions): BackendClien } }; - const getClient = async (): Promise => { - if (clientPromise) { - return clientPromise; - } - - clientPromise = (async () => { - // Use the serverless /metadata endpoint to discover the manager endpoint. - // If the server reports a loopback clientEndpoint (127.0.0.1), rewrite to the same host - // as the configured endpoint so remote browsers/clients can connect. - const configured = new URL(rivetApiEndpoint); - const configuredOrigin = `${configured.protocol}//${configured.host}`; - - const initialNamespace = undefined; - const metadata = await fetchMetadataWithRetry(rivetApiEndpoint, initialNamespace, { - timeoutMs: 30_000, - requestTimeoutMs: 8_000, - }); - - // Candidate endpoint: manager endpoint if provided, otherwise stick to the configured endpoint. - const candidateEndpoint = metadata.clientEndpoint ? rewriteLoopbackClientEndpoint(metadata.clientEndpoint, configuredOrigin) : rivetApiEndpoint; - - // If the manager port isn't reachable from this client (common behind reverse proxies), - // fall back to the configured serverless endpoint to avoid hanging requests. - const shouldUseCandidate = metadata.clientEndpoint ? await probeMetadataEndpoint(candidateEndpoint, metadata.clientNamespace, 1_500) : true; - const resolvedEndpoint = shouldUseCandidate ? candidateEndpoint : rivetApiEndpoint; - - return createClient({ - endpoint: resolvedEndpoint, - namespace: metadata.clientNamespace, - token: metadata.clientToken, - // Prevent rivetkit from overriding back to a loopback endpoint (or to an unreachable manager). - disableMetadataLookup: true, - }) as unknown as RivetClient; - })(); - - return clientPromise; - }; - const workspace = async (workspaceId: string): Promise => - (await getClient()).workspace.getOrCreate(workspaceKey(workspaceId), { + client.workspace.getOrCreate(workspaceKey(workspaceId), { createWithInput: workspaceId, }); const sandboxByKey = async (workspaceId: string, providerId: ProviderId, sandboxId: string): Promise => { - const client = await getClient(); return (client as any).sandboxInstance.get(sandboxInstanceKey(workspaceId, providerId, sandboxId)); }; @@ -557,7 +365,6 @@ export function createBackendClient(options: BackendClientOptions): BackendClien (sb as any).sandboxActorId.length > 0, ) as { sandboxActorId?: string } | undefined; if (sandbox?.sandboxActorId) { - const client = await getClient(); return (client as any).sandboxInstance.getForId(sandbox.sandboxActorId); } } catch (error) { @@ -698,17 +505,62 @@ export function createBackendClient(options: BackendClientOptions): BackendClien }; }; + const subscribeApp = (listener: () => void): (() => void) => { + appSubscriptions.listeners.add(listener); + + if (!appSubscriptions.disposeConnPromise) { + appSubscriptions.disposeConnPromise = (async () => { + const handle = await workspace("app"); + const conn = (handle as any).connect(); + const unsubscribeEvent = conn.on("appUpdated", () => { + for (const currentListener of [...appSubscriptions.listeners]) { + currentListener(); + } + }); + const unsubscribeError = conn.onError(() => {}); + return async () => { + unsubscribeEvent(); + unsubscribeError(); + await conn.dispose(); + }; + })().catch(() => null); + } + + return () => { + appSubscriptions.listeners.delete(listener); + if (appSubscriptions.listeners.size > 0) { + return; + } + + void appSubscriptions.disposeConnPromise?.then(async (disposeConn) => { + await disposeConn?.(); + }); + appSubscriptions.disposeConnPromise = null; + }; + }; + return { async getAppSnapshot(): Promise { return await appRequest("/app/snapshot"); }, + subscribeApp(listener: () => void): () => void { + return subscribeApp(listener); + }, + async signInWithGithub(): Promise { + const callbackURL = typeof window !== "undefined" ? `${window.location.origin}/organizations` : `${appApiEndpoint.replace(/\/$/, "")}/organizations`; + const response = await appRequest<{ url: string; redirect?: boolean }>("/auth/sign-in/social", { + method: "POST", + body: JSON.stringify({ + provider: "github", + callbackURL, + disableRedirect: true, + }), + }); if (typeof window !== "undefined") { - window.location.assign(`${appApiEndpoint}/auth/github/start`); - return; + window.location.assign(response.url); } - await redirectTo("/auth/github/start"); }, async signOutApp(): Promise { diff --git a/foundry/packages/client/src/mock/backend-client.ts b/foundry/packages/client/src/mock/backend-client.ts index 7b0ad7f..6f5e7d3 100644 --- a/foundry/packages/client/src/mock/backend-client.ts +++ b/foundry/packages/client/src/mock/backend-client.ts @@ -192,6 +192,10 @@ export function createMockBackendClient(defaultWorkspaceId = "default"): Backend return unsupportedAppSnapshot(); }, + subscribeApp(_listener: () => void): () => void { + return () => {}; + }, + async signInWithGithub(): Promise { notSupported("signInWithGithub"); }, diff --git a/foundry/packages/client/src/remote/app-client.ts b/foundry/packages/client/src/remote/app-client.ts index e381540..9b80f3c 100644 --- a/foundry/packages/client/src/remote/app-client.ts +++ b/foundry/packages/client/src/remote/app-client.ts @@ -25,7 +25,7 @@ class RemoteFoundryAppStore implements FoundryAppClient { }; private readonly listeners = new Set<() => void>(); private refreshPromise: Promise | null = null; - private syncPollTimeout: ReturnType | null = null; + private unsubscribeApp: (() => void) | null = null; constructor(options: RemoteFoundryAppClientOptions) { this.backend = options.backend; @@ -37,9 +37,13 @@ class RemoteFoundryAppStore implements FoundryAppClient { subscribe(listener: () => void): () => void { this.listeners.add(listener); - void this.refresh(); + this.ensureStarted(); return () => { this.listeners.delete(listener); + if (this.listeners.size === 0 && this.unsubscribeApp) { + this.unsubscribeApp(); + this.unsubscribeApp = null; + } }; } @@ -66,7 +70,6 @@ class RemoteFoundryAppStore implements FoundryAppClient { async selectOrganization(organizationId: string): Promise { this.snapshot = await this.backend.selectAppOrganization(organizationId); this.notify(); - this.scheduleSyncPollingIfNeeded(); } async updateOrganizationProfile(input: UpdateFoundryOrganizationProfileInput): Promise { @@ -77,7 +80,6 @@ class RemoteFoundryAppStore implements FoundryAppClient { async triggerGithubSync(organizationId: string): Promise { this.snapshot = await this.backend.triggerAppRepoImport(organizationId); this.notify(); - this.scheduleSyncPollingIfNeeded(); } async completeHostedCheckout(organizationId: string, planId: FoundryBillingPlanId): Promise { @@ -107,20 +109,13 @@ class RemoteFoundryAppStore implements FoundryAppClient { this.notify(); } - private scheduleSyncPollingIfNeeded(): void { - if (this.syncPollTimeout) { - clearTimeout(this.syncPollTimeout); - this.syncPollTimeout = null; + private ensureStarted(): void { + if (!this.unsubscribeApp) { + this.unsubscribeApp = this.backend.subscribeApp(() => { + void this.refresh(); + }); } - - if (!this.snapshot.organizations.some((organization) => organization.github.syncStatus === "syncing")) { - return; - } - - this.syncPollTimeout = setTimeout(() => { - this.syncPollTimeout = null; - void this.refresh(); - }, 500); + void this.refresh(); } private async refresh(): Promise { @@ -132,7 +127,6 @@ class RemoteFoundryAppStore implements FoundryAppClient { this.refreshPromise = (async () => { this.snapshot = await this.backend.getAppSnapshot(); this.notify(); - this.scheduleSyncPollingIfNeeded(); })().finally(() => { this.refreshPromise = null; }); diff --git a/foundry/packages/frontend/src/components/mock-layout.tsx b/foundry/packages/frontend/src/components/mock-layout.tsx index baab797..af8b84e 100644 --- a/foundry/packages/frontend/src/components/mock-layout.tsx +++ b/foundry/packages/frontend/src/components/mock-layout.tsx @@ -727,7 +727,7 @@ const RightRail = memo(function RightRail({ }, [clampTerminalHeight]); const startResize = useCallback( - (event: ReactPointerEvent) => { + (event: ReactPointerEvent) => { event.preventDefault(); const startY = event.clientY; @@ -914,7 +914,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M const activeOrg = activeMockOrganization(appSnapshot); const navigateToUsage = useCallback(() => { if (activeOrg) { - void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } }); + void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: activeOrg.id } as never }); } }, [activeOrg, navigate]); const [projectOrder, setProjectOrder] = useState(null); @@ -1283,7 +1283,11 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M const onDragMouseDown = useCallback((event: ReactPointerEvent) => { if (event.button !== 0) return; // Tauri v2 IPC: invoke start_dragging on the webview window - const ipc = (window as Record).__TAURI_INTERNALS__ as { invoke: (cmd: string, args?: unknown) => Promise } | undefined; + const ipc = (window as unknown as Record).__TAURI_INTERNALS__ as + | { + invoke: (cmd: string, args?: unknown) => Promise; + } + | undefined; if (ipc?.invoke) { ipc.invoke("plugin:window|start_dragging").catch(() => {}); } diff --git a/foundry/packages/frontend/src/components/mock-layout/terminal-pane.tsx b/foundry/packages/frontend/src/components/mock-layout/terminal-pane.tsx index d0e011d..c27cc56 100644 --- a/foundry/packages/frontend/src/components/mock-layout/terminal-pane.tsx +++ b/foundry/packages/frontend/src/components/mock-layout/terminal-pane.tsx @@ -135,6 +135,9 @@ export function TerminalPane({ workspaceId, taskId, isExpanded, onExpand, onColl setProcessTabs((prev) => { const next = [...prev]; const [moved] = next.splice(d.fromIdx, 1); + if (!moved) { + return prev; + } next.splice(d.overIdx!, 0, moved); return next; }); diff --git a/foundry/packages/frontend/src/components/mock-onboarding.tsx b/foundry/packages/frontend/src/components/mock-onboarding.tsx index f583397..66bcfcc 100644 --- a/foundry/packages/frontend/src/components/mock-onboarding.tsx +++ b/foundry/packages/frontend/src/components/mock-onboarding.tsx @@ -56,7 +56,11 @@ function DesktopDragRegion() { const isDesktop = !!import.meta.env.VITE_DESKTOP; const onDragMouseDown = useCallback((event: React.PointerEvent) => { if (event.button !== 0) return; - const ipc = (window as Record).__TAURI_INTERNALS__ as { invoke: (cmd: string, args?: unknown) => Promise } | undefined; + const ipc = (window as unknown as Record).__TAURI_INTERNALS__ as + | { + invoke: (cmd: string, args?: unknown) => Promise; + } + | undefined; if (ipc?.invoke) { ipc.invoke("plugin:window|start_dragging").catch(() => {}); } diff --git a/foundry/packages/shared/src/contracts.ts b/foundry/packages/shared/src/contracts.ts index 695a10f..6c99d4e 100644 --- a/foundry/packages/shared/src/contracts.ts +++ b/foundry/packages/shared/src/contracts.ts @@ -172,6 +172,23 @@ export const RepoOverviewSchema = z.object({ baseRef: z.string().nullable(), stackAvailable: z.boolean(), fetchedAt: z.number().int(), + branchSyncAt: z.number().int().nullable(), + prSyncAt: z.number().int().nullable(), + branchSyncStatus: z.enum(["pending", "syncing", "synced", "error"]), + prSyncStatus: z.enum(["pending", "syncing", "synced", "error"]), + repoActionJobs: z.array( + z.object({ + jobId: z.string().min(1), + action: z.enum(["sync_repo", "restack_repo", "restack_subtree", "rebase_branch", "reparent_branch"]), + branchName: z.string().nullable(), + parentBranch: z.string().nullable(), + status: z.enum(["queued", "running", "completed", "error"]), + message: z.string().min(1), + createdAt: z.number().int(), + updatedAt: z.number().int(), + completedAt: z.number().int().nullable(), + }), + ), branches: z.array(RepoBranchRecordSchema), }); export type RepoOverview = z.infer; @@ -189,8 +206,10 @@ export const RepoStackActionInputSchema = z.object({ export type RepoStackActionInput = z.infer; export const RepoStackActionResultSchema = z.object({ + jobId: z.string().min(1).nullable().optional(), action: RepoStackActionSchema, executed: z.boolean(), + status: z.enum(["queued", "running", "completed", "error"]).optional(), message: z.string().min(1), at: z.number().int(), }); diff --git a/justfile b/justfile index 7ff63a6..e1242f9 100644 --- a/justfile +++ b/justfile @@ -127,75 +127,35 @@ foundry-check: foundry-dev: pnpm install mkdir -p foundry/.foundry/logs - HF_DOCKER_UID="$(id -u)" HF_DOCKER_GID="$(id -g)" docker compose -f foundry/compose.dev.yaml up --build --force-recreate -d + HF_DOCKER_UID="$(id -u)" HF_DOCKER_GID="$(id -g)" docker compose --env-file .env -f foundry/compose.dev.yaml up --build --force-recreate -d [group('foundry')] foundry-preview: pnpm install mkdir -p foundry/.foundry/logs - HF_DOCKER_UID="$(id -u)" HF_DOCKER_GID="$(id -g)" docker compose -f foundry/compose.preview.yaml up --build --force-recreate -d - -[group('foundry')] -foundry-frontend-dev host='127.0.0.1' port='4173' backend='http://127.0.0.1:7741/api/rivet': - pnpm install - VITE_HF_BACKEND_ENDPOINT="{{backend}}" pnpm --filter @sandbox-agent/foundry-frontend dev -- --host {{host}} --port {{port}} - -[group('foundry')] -foundry-dev-mock host='127.0.0.1' port='4173': - pnpm install - FOUNDRY_FRONTEND_CLIENT_MODE=mock pnpm --filter @sandbox-agent/foundry-frontend dev -- --host {{host}} --port {{port}} - -[group('foundry')] -foundry-dev-turbo: - pnpm exec turbo run dev --parallel --filter=@sandbox-agent/foundry-* + HF_DOCKER_UID="$(id -u)" HF_DOCKER_GID="$(id -g)" docker compose --env-file .env -f foundry/compose.preview.yaml up --build --force-recreate -d [group('foundry')] foundry-dev-down: - docker compose -f foundry/compose.dev.yaml down + docker compose --env-file .env -f foundry/compose.dev.yaml down [group('foundry')] foundry-dev-logs: - docker compose -f foundry/compose.dev.yaml logs -f --tail=200 + docker compose --env-file .env -f foundry/compose.dev.yaml logs -f --tail=200 [group('foundry')] foundry-preview-down: - docker compose -f foundry/compose.preview.yaml down + docker compose --env-file .env -f foundry/compose.preview.yaml down [group('foundry')] foundry-preview-logs: - docker compose -f foundry/compose.preview.yaml logs -f --tail=200 + docker compose --env-file .env -f foundry/compose.preview.yaml logs -f --tail=200 [group('foundry')] foundry-format: prettier --write foundry -[group('foundry')] -foundry-backend-start host='127.0.0.1' port='7741': - pnpm install - pnpm --filter @sandbox-agent/foundry-backend build - pnpm --filter @sandbox-agent/foundry-backend start -- --host {{host}} --port {{port}} - -[group('foundry')] -foundry-hf *ARGS: - @echo "CLI package is disabled in this repo; use frontend workflows instead." >&2 - @exit 1 - [group('foundry')] foundry-docker-build tag='foundry:local': docker build -f foundry/docker/backend.Dockerfile -t {{tag}} . -[group('foundry')] -foundry-desktop-dev: - pnpm --filter @sandbox-agent/foundry-desktop dev - -[group('foundry')] -foundry-desktop-build: - pnpm --filter @sandbox-agent/foundry-desktop build:all - -[group('foundry')] -foundry-railway-up: - npx -y @railway/cli up --detach - -[group('foundry')] -foundry-railway-status: - npx -y @railway/cli status --json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e2854b..2d1768b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,13 +29,13 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@1.21.7)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) examples/boxlite: dependencies: '@boxlite-ai/boxlite': specifier: latest - version: 0.3.0 + version: 0.4.1 '@sandbox-agent/example-shared': specifier: workspace:* version: link:../shared @@ -45,7 +45,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -73,10 +73,10 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: latest - version: 4.20260310.1 + version: 4.20260313.1 '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 '@types/react': specifier: ^18.3.3 version: 18.3.27 @@ -85,19 +85,19 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^4.5.0 - version: 4.7.0(vite@6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) typescript: specifier: latest version: 5.9.3 vite: specifier: ^6.2.0 - version: 6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) wrangler: specifier: latest - version: 4.72.0(@cloudflare/workers-types@4.20260310.1) + version: 4.73.0(@cloudflare/workers-types@4.20260313.1) examples/computesdk: dependencies: @@ -106,14 +106,14 @@ importers: version: link:../shared computesdk: specifier: latest - version: 2.3.0 + version: 2.5.0 sandbox-agent: specifier: workspace:* version: link:../../sdks/typescript devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -122,13 +122,13 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) examples/daytona: dependencies: '@daytonaio/sdk': specifier: latest - version: 0.150.0(ws@8.19.0) + version: 0.151.0(ws@8.19.0) '@sandbox-agent/example-shared': specifier: workspace:* version: link:../shared @@ -138,7 +138,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -163,7 +163,7 @@ importers: version: 4.0.1 '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -172,7 +172,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) examples/e2b: dependencies: @@ -188,7 +188,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -197,7 +197,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) examples/file-system: dependencies: @@ -213,7 +213,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -232,7 +232,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -257,10 +257,10 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 esbuild: specifier: latest - version: 0.27.3 + version: 0.27.4 tsx: specifier: latest version: 4.21.0 @@ -272,7 +272,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 typescript: specifier: latest version: 5.9.3 @@ -288,7 +288,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -307,7 +307,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -332,7 +332,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 '@types/pg': specifier: latest version: 8.18.0 @@ -357,7 +357,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -379,7 +379,7 @@ importers: version: 4.0.1 '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 typescript: specifier: latest version: 5.9.3 @@ -395,7 +395,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -414,10 +414,10 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 esbuild: specifier: latest - version: 0.27.3 + version: 0.27.4 tsx: specifier: latest version: 4.21.0 @@ -439,7 +439,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -448,7 +448,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) foundry/packages/backend: dependencies: @@ -470,12 +470,15 @@ importers: '@sandbox-agent/persist-rivet': specifier: workspace:* version: link:../../../sdks/persist-rivet + better-auth: + specifier: ^1.5.5 + version: 1.5.5(@cloudflare/workers-types@4.20260313.1)(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) drizzle-kit: specifier: ^0.31.8 version: 0.31.9 drizzle-orm: specifier: ^0.44.5 - version: 0.44.7(@cloudflare/workers-types@4.20260310.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(pg@8.20.0) + version: 0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0) hono: specifier: ^4.11.9 version: 4.12.2 @@ -484,7 +487,7 @@ importers: version: 10.3.1 rivetkit: specifier: 2.1.6 - version: 2.1.6(@hono/node-server@1.19.9(hono@4.12.2))(@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260310.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(pg@8.20.0))(ws@8.19.0) + version: 2.1.6(@hono/node-server@1.19.9(hono@4.12.2))(@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2))(@standard-schema/spec@1.1.0)(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0))(ws@8.19.0) sandbox-agent: specifier: workspace:* version: link:../../../sdks/typescript @@ -512,7 +515,7 @@ importers: version: link:../shared rivetkit: specifier: 2.1.6 - version: 2.1.6(@hono/node-server@1.19.9(hono@4.12.2))(@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260310.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(pg@8.20.0))(ws@8.19.0) + version: 2.1.6(@hono/node-server@1.19.9(hono@4.12.2))(@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2))(@standard-schema/spec@1.1.0)(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0))(ws@8.19.0) sandbox-agent: specifier: workspace:* version: link:../../../sdks/typescript @@ -593,7 +596,7 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^5.0.3 - version: 5.1.4(vite@7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.1.4(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) react-grab: specifier: ^0.1.13 version: 0.1.27(@types/react@18.3.27)(react@19.2.4) @@ -602,7 +605,7 @@ importers: version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) vite: specifier: ^7.1.3 - version: 7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) foundry/packages/frontend-errors: dependencies: @@ -618,7 +621,7 @@ importers: version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) vite: specifier: ^7.1.3 - version: 7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) foundry/packages/shared: dependencies: @@ -659,7 +662,7 @@ importers: version: 18.3.7(@types/react@18.3.27) '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.7.0(vite@5.4.21(@types/node@25.4.0)) + version: 4.7.0(vite@5.4.21(@types/node@25.5.0)) fake-indexeddb: specifier: ^6.2.4 version: 6.2.5 @@ -671,25 +674,25 @@ importers: version: 5.9.3 vite: specifier: ^5.4.7 - version: 5.4.21(@types/node@25.4.0) + version: 5.4.21(@types/node@25.5.0) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) frontend/packages/website: dependencies: '@astrojs/react': specifier: ^4.2.0 - version: 4.4.2(@types/node@25.4.0)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2) + version: 4.4.2(@types/node@25.5.0)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2) '@astrojs/sitemap': specifier: ^3.2.0 version: 3.7.0 '@astrojs/tailwind': specifier: ^6.0.0 - version: 6.0.2(astro@5.16.15(@types/node@25.4.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.2(astro@5.16.15(@types/node@25.5.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) astro: specifier: ^5.1.0 - version: 5.16.15(@types/node@25.4.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 5.16.15(@types/node@25.5.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) framer-motion: specifier: ^12.0.0 version: 12.29.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -773,14 +776,14 @@ importers: dependencies: '@daytonaio/sdk': specifier: latest - version: 0.150.0(ws@8.19.0) + version: 0.151.0(ws@8.19.0) '@e2b/code-interpreter': specifier: latest version: 2.3.3 devDependencies: '@types/node': specifier: latest - version: 25.4.0 + version: 25.5.0 tsx: specifier: latest version: 4.21.0 @@ -831,7 +834,7 @@ importers: devDependencies: vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) sdks/cli-shared: devDependencies: @@ -879,7 +882,7 @@ importers: devDependencies: vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) sdks/gigacode/platforms/darwin-arm64: {} @@ -1053,7 +1056,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -1414,6 +1417,74 @@ packages: '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@better-auth/core@1.5.5': + resolution: {integrity: sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA==} + peerDependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' + better-call: 1.3.2 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + '@better-auth/drizzle-adapter@1.5.5': + resolution: {integrity: sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw==} + peerDependencies: + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + drizzle-orm: '>=0.41.0' + peerDependenciesMeta: + drizzle-orm: + optional: true + + '@better-auth/kysely-adapter@1.5.5': + resolution: {integrity: sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g==} + peerDependencies: + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + kysely: ^0.27.0 || ^0.28.0 + + '@better-auth/memory-adapter@1.5.5': + resolution: {integrity: sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g==} + peerDependencies: + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + + '@better-auth/mongo-adapter@1.5.5': + resolution: {integrity: sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg==} + peerDependencies: + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + mongodb: ^6.0.0 || ^7.0.0 + + '@better-auth/prisma-adapter@1.5.5': + resolution: {integrity: sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==} + peerDependencies: + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@prisma/client': + optional: true + prisma: + optional: true + + '@better-auth/telemetry@1.5.5': + resolution: {integrity: sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ==} + peerDependencies: + '@better-auth/core': 1.5.5 + + '@better-auth/utils@0.3.1': + resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@biomejs/biome@2.4.6': resolution: {integrity: sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==} engines: {node: '>=14.21.3'} @@ -1467,20 +1538,20 @@ packages: cpu: [x64] os: [win32] - '@boxlite-ai/boxlite-darwin-arm64@0.3.0': - resolution: {integrity: sha512-V0FeD7VTQ+V4LFAwHzSe2K7hl7IjXKS6u1VrWr/H0zJ8GGZTAi6feI1w+QTmLJMgdlJdIufWsJwY/RsjtwwF/Q==} + '@boxlite-ai/boxlite-darwin-arm64@0.4.1': + resolution: {integrity: sha512-OcpQ9fGeQTBLZDOwireul0l6D+kTxs8Qmtalv/hobBUto8cOLX7NjiQ1Huo9kr0UOThu1T8nfDSNol18+iOjpw==} engines: {node: '>=18.0.0'} cpu: [arm64] os: [darwin] - '@boxlite-ai/boxlite-linux-x64-gnu@0.3.0': - resolution: {integrity: sha512-1VkXxzm+3hmuP6XpbZxPsaf+Tv2gwd7iHAH76f2uWulooxRjATnk+Smhud+FuHvLQIvjr8ERAA26vMbST5OgpQ==} + '@boxlite-ai/boxlite-linux-x64-gnu@0.4.1': + resolution: {integrity: sha512-hBoZxWRvkFS8sztOjtTtgIAEE0K2xzuBtte2hl0sLfg5twgCy2BE/Ds/RikC6hMk6Ug4oc8MeBfWIhSvF70Jjw==} engines: {node: '>=18.0.0'} cpu: [x64] os: [linux] - '@boxlite-ai/boxlite@0.3.0': - resolution: {integrity: sha512-D9sU7PUzFHlgv6aIGf+h5kp0+C2A05RVX73aaSMK2gWjQgf12lJ/SVg3OiMSmhnV0cZ+Q0hTn+EBnDWpe26cqA==} + '@boxlite-ai/boxlite@0.4.1': + resolution: {integrity: sha512-iy302L3Yy4w8UTYQrthYzB0KGFh8y71olSUYnseUnnIQlCgHBlFHCdrdPrgUrttplBu/m4zwTRNCQq4jIzNWeg==} engines: {node: '>=18.0.0'} peerDependencies: playwright-core: '>=1.58.0' @@ -1555,38 +1626,38 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260310.1': - resolution: {integrity: sha512-hF2VpoWaMb1fiGCQJqCY6M8I+2QQqjkyY4LiDYdTL5D/w6C1l5v1zhc0/jrjdD1DXfpJtpcSMSmEPjHse4p9Ig==} + '@cloudflare/workerd-darwin-64@1.20260312.1': + resolution: {integrity: sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260310.1': - resolution: {integrity: sha512-h/Vl3XrYYPI6yFDE27XO1QPq/1G1lKIM8tzZGIWYpntK3IN5XtH3Ee/sLaegpJ49aIJoqhF2mVAZ6Yw+Vk2gJw==} + '@cloudflare/workerd-darwin-arm64@1.20260312.1': + resolution: {integrity: sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260310.1': - resolution: {integrity: sha512-XzQ0GZ8G5P4d74bQYOIP2Su4CLdNPpYidrInaSOuSxMw+HamsHaFrjVsrV2mPy/yk2hi6SY2yMbgKFK9YjA7vw==} + '@cloudflare/workerd-linux-64@1.20260312.1': + resolution: {integrity: sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260310.1': - resolution: {integrity: sha512-sxv4CxnN4ZR0uQGTFVGa0V4KTqwdej/czpIc5tYS86G8FQQoGIBiAIs2VvU7b8EROPcandxYHDBPTb+D9HIMPw==} + '@cloudflare/workerd-linux-arm64@1.20260312.1': + resolution: {integrity: sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260310.1': - resolution: {integrity: sha512-+1ZTViWKJypLfgH/luAHCqkent0DEBjAjvO40iAhOMHRLYP/SPphLvr4Jpi6lb+sIocS8Q1QZL4uM5Etg1Wskg==} + '@cloudflare/workerd-windows-64@1.20260312.1': + resolution: {integrity: sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20260310.1': - resolution: {integrity: sha512-Cg4gyGDtfimNMgBr2h06aGR5Bt8puUbblyzPNZN55mBfVYCTWwQiUd9PrbkcoddKrWHlsy0ACH/16dAeGf5BQg==} + '@cloudflare/workers-types@4.20260313.1': + resolution: {integrity: sha512-jMEeX3RKfOSVqqXRKr/ulgglcTloeMzSH3FdzIfqJHtvc12/ELKd5Ldsg8ZHahKX/4eRxYdw3kbzb8jLXbq/jQ==} '@computesdk/cmd@0.4.1': resolution: {integrity: sha512-hhcYrwMnOpRSwWma3gkUeAVsDFG56nURwSaQx8vCepv0IuUv39bK4mMkgszolnUQrVjBDdW7b3lV+l5B2S8fRA==} @@ -1628,20 +1699,20 @@ packages: '@daytonaio/api-client@0.141.0': resolution: {integrity: sha512-DSPCurIEjfFyXCd07jkDgfsoFppVhTLyIJdvfb0LgG1EgV75BPqqzk2WM4ragBFJUuK2URF5CK7qkaHW0AXKMA==} - '@daytonaio/api-client@0.150.0': - resolution: {integrity: sha512-NXGE1sgd8+VBzu3B7P/pLrlpci9nMoZecvLmK3zFDh8hr5Ra5vuXJN9pEVJmev93zUItQxHbuvaxaWrYzHevVA==} + '@daytonaio/api-client@0.151.0': + resolution: {integrity: sha512-Ahu7bjunHbJEEAEkcEFjjdazN+1hML/lLZwOyul2WFaCTh9q5dmufhr0qKAKCIs3ccTY+Is0fO5UtPpo/fig+A==} '@daytonaio/sdk@0.141.0': resolution: {integrity: sha512-JUopkS9SkO7h4WN8CjparOrP9k954euOF5KG//PeCEFOxUWTPFOME70GrmHXQKa1qkdZiF/4tz9jtZ744B1I2w==} - '@daytonaio/sdk@0.150.0': - resolution: {integrity: sha512-JmNulFaLhmpjVVFtaRDZa84fxPuy0axQYVLrj1lvRgcZzcrwJRdHv9FZPMLbKdrbicMh3D7GYA9XeBMYVZBTIg==} + '@daytonaio/sdk@0.151.0': + resolution: {integrity: sha512-wd4x9Bipt1KmTD+0GXTVEQtgXBmyy/gAmCjdOJllwo5Ya5RbGu/CZeitBCIEKhDM8TnkxefVxdpxBCfi/Wg9xA==} '@daytonaio/toolbox-api-client@0.141.0': resolution: {integrity: sha512-KGkCLDLAltd9FCic3PhSJGrTp3RwGsUwWEGp5vyWZFQGWpJV8CVp08CH5SBdo4YhuqFUVlyQcwha1HpzpVH++A==} - '@daytonaio/toolbox-api-client@0.150.0': - resolution: {integrity: sha512-7MCbD1FrzYjOaOmqpMDQe7cyoQTSImEOjQ+6Js4NlBOwPlz2PMi352XuG9qrBp9ngNpo8fpduYr35iDOjrpIVg==} + '@daytonaio/toolbox-api-client@0.151.0': + resolution: {integrity: sha512-63n/wBNnZh1r8dUypzwNeenoA4okWNEWzsE6kZ8b047y5zBYT0cI63cGRn25nSrepLlGKpX4MJnVjjz50+bVqA==} '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -1685,6 +1756,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -1715,6 +1792,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -1745,6 +1828,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -1775,6 +1864,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -1805,6 +1900,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -1835,6 +1936,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -1865,6 +1972,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -1895,6 +2008,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -1925,6 +2044,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -1955,6 +2080,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -1985,6 +2116,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -2015,6 +2152,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -2045,6 +2188,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -2075,6 +2224,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -2105,6 +2260,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -2135,6 +2296,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -2165,6 +2332,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -2183,6 +2356,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -2213,6 +2392,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -2231,6 +2416,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -2261,6 +2452,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -2279,6 +2476,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -2309,6 +2512,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -2339,6 +2548,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -2369,6 +2584,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -2399,6 +2620,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -2675,6 +2902,14 @@ packages: '@cfworker/json-schema': optional: true + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3355,6 +3590,9 @@ packages: '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tanstack/history@1.161.4': resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} engines: {node: '>=20.19'} @@ -3530,8 +3768,8 @@ packages: '@types/node@24.10.9': resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} - '@types/node@25.4.0': - resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} '@types/pg@8.16.0': resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} @@ -3781,6 +4019,76 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + better-auth@1.5.5: + resolution: {integrity: sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.3.2: + resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} @@ -4009,8 +4317,8 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - computesdk@2.3.0: - resolution: {integrity: sha512-4B7CRN2qB6XkuAnN7dZ0aMYqHaFrh2qdSuh02lM+cgMEQ7wZy9v44FAjBGfWebHXuPNA/nZRx7211U6CEiGdTw==} + computesdk@2.5.0: + resolution: {integrity: sha512-HDOSw1D6ogR7wwlBtr5ZZtRFhfNxdPBW1omLaApxDwAvdkBW4PGoIfYyMPX0N1LpaDl5NWwWALMvmtW2p4dafg==} confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -4541,6 +4849,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -5100,6 +5413,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + lefthook-darwin-arm64@2.1.3: resolution: {integrity: sha512-VMSQK5ZUh66mKrEpHt5U81BxOg5xAXLoLZIK6e++4uc28tj8zGBqV9+tZqSRElXXzlnHbfdDVCMaKlTuqUy0Rg==} cpu: [arm64] @@ -5401,8 +5718,8 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - miniflare@4.20260310.0: - resolution: {integrity: sha512-uC5vNPenFpDSj5aUU3wGSABG6UUqMr+Xs1m4AkCrTHo37F4Z6xcQw5BXqViTfPDVT/zcYH1UgTVoXhr1l6ZMXw==} + miniflare@4.20260312.0: + resolution: {integrity: sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w==} engines: {node: '>=18.0.0'} hasBin: true @@ -5475,6 +5792,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanostores@1.1.1: + resolution: {integrity: sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg==} + engines: {node: ^20.0.0 || >=22.0.0} + napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -6200,6 +6521,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -6257,6 +6581,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -7051,17 +7378,17 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} - workerd@1.20260310.1: - resolution: {integrity: sha512-yawXhypXXHtArikJj15HOMknNGikpBbSg2ZDe6lddUbqZnJXuCVSkgc/0ArUeVMG1jbbGvpst+REFtKwILvRTQ==} + workerd@1.20260312.1: + resolution: {integrity: sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg==} engines: {node: '>=16'} hasBin: true - wrangler@4.72.0: - resolution: {integrity: sha512-bKkb8150JGzJZJWiNB2nu/33smVfawmfYiecA6rW4XH7xS23/jqMbgpdelM34W/7a1IhR66qeQGVqTRXROtAZg==} + wrangler@4.73.0: + resolution: {integrity: sha512-VJXsqKDFCp6OtFEHXITSOR5kh95JOknwPY8m7RyQuWJQguSybJy43m4vhoCSt42prutTef7eeuw7L4V4xiynGw==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260310.1 + '@cloudflare/workers-types': ^4.20260312.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -7234,15 +7561,15 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@4.4.2(@types/node@25.4.0)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)': + '@astrojs/react@4.4.2(@types/node@25.5.0)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(jiti@1.21.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.8.2)': dependencies: '@types/react': 18.3.27 '@types/react-dom': 18.3.7(@types/react@18.3.27) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) ultrahtml: 1.6.0 - vite: 6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -7263,9 +7590,9 @@ snapshots: stream-replace-string: 2.0.0 zod: 3.25.76 - '@astrojs/tailwind@6.0.2(astro@5.16.15(@types/node@25.4.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))': + '@astrojs/tailwind@6.0.2(astro@5.16.15(@types/node@25.5.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))': dependencies: - astro: 5.16.15(@types/node@25.4.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + astro: 5.16.15(@types/node@25.5.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) autoprefixer: 10.4.23(postcss@8.5.6) postcss: 8.5.6 postcss-load-config: 4.0.2(postcss@8.5.6) @@ -8019,6 +8346,57 @@ snapshots: '@balena/dockerignore@1.0.2': {} + '@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)': + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 + better-call: 1.3.2(zod@4.3.6) + jose: 6.1.3 + kysely: 0.28.11 + nanostores: 1.1.1 + zod: 4.3.6 + optionalDependencies: + '@cloudflare/workers-types': 4.20260313.1 + + '@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0))': + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + optionalDependencies: + drizzle-orm: 0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0) + + '@better-auth/kysely-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + kysely: 0.28.11 + + '@better-auth/memory-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + + '@better-auth/mongo-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + + '@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + + '@better-auth/telemetry@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))': + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.1': {} + + '@better-fetch/fetch@1.1.21': {} + '@biomejs/biome@2.4.6': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.4.6 @@ -8054,16 +8432,16 @@ snapshots: '@biomejs/cli-win32-x64@2.4.6': optional: true - '@boxlite-ai/boxlite-darwin-arm64@0.3.0': + '@boxlite-ai/boxlite-darwin-arm64@0.4.1': optional: true - '@boxlite-ai/boxlite-linux-x64-gnu@0.3.0': + '@boxlite-ai/boxlite-linux-x64-gnu@0.4.1': optional: true - '@boxlite-ai/boxlite@0.3.0': + '@boxlite-ai/boxlite@0.4.1': optionalDependencies: - '@boxlite-ai/boxlite-darwin-arm64': 0.3.0 - '@boxlite-ai/boxlite-linux-x64-gnu': 0.3.0 + '@boxlite-ai/boxlite-darwin-arm64': 0.4.1 + '@boxlite-ai/boxlite-linux-x64-gnu': 0.4.1 '@bufbuild/protobuf@2.11.0': {} @@ -8100,28 +8478,28 @@ snapshots: optionalDependencies: '@opencode-ai/sdk': 1.2.24 - '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260310.1)': + '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260312.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260310.1 + workerd: 1.20260312.1 - '@cloudflare/workerd-darwin-64@1.20260310.1': + '@cloudflare/workerd-darwin-64@1.20260312.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260310.1': + '@cloudflare/workerd-darwin-arm64@1.20260312.1': optional: true - '@cloudflare/workerd-linux-64@1.20260310.1': + '@cloudflare/workerd-linux-64@1.20260312.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260310.1': + '@cloudflare/workerd-linux-arm64@1.20260312.1': optional: true - '@cloudflare/workerd-windows-64@1.20260310.1': + '@cloudflare/workerd-windows-64@1.20260312.1': optional: true - '@cloudflare/workers-types@4.20260310.1': {} + '@cloudflare/workers-types@4.20260313.1': {} '@computesdk/cmd@0.4.1': {} @@ -8158,7 +8536,7 @@ snapshots: transitivePeerDependencies: - debug - '@daytonaio/api-client@0.150.0': + '@daytonaio/api-client@0.151.0': dependencies: axios: 1.13.5 transitivePeerDependencies: @@ -8195,12 +8573,12 @@ snapshots: - supports-color - ws - '@daytonaio/sdk@0.150.0(ws@8.19.0)': + '@daytonaio/sdk@0.151.0(ws@8.19.0)': dependencies: '@aws-sdk/client-s3': 3.975.0 '@aws-sdk/lib-storage': 3.975.0(@aws-sdk/client-s3@3.975.0) - '@daytonaio/api-client': 0.150.0 - '@daytonaio/toolbox-api-client': 0.150.0 + '@daytonaio/api-client': 0.151.0 + '@daytonaio/toolbox-api-client': 0.151.0 '@iarna/toml': 2.2.5 '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-http': 0.207.0(@opentelemetry/api@1.9.0) @@ -8232,7 +8610,7 @@ snapshots: transitivePeerDependencies: - debug - '@daytonaio/toolbox-api-client@0.150.0': + '@daytonaio/toolbox-api-client@0.151.0': dependencies: axios: 1.13.5 transitivePeerDependencies: @@ -8271,6 +8649,9 @@ snapshots: '@esbuild/aix-ppc64@0.27.3': optional: true + '@esbuild/aix-ppc64@0.27.4': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true @@ -8286,6 +8667,9 @@ snapshots: '@esbuild/android-arm64@0.27.3': optional: true + '@esbuild/android-arm64@0.27.4': + optional: true + '@esbuild/android-arm@0.18.20': optional: true @@ -8301,6 +8685,9 @@ snapshots: '@esbuild/android-arm@0.27.3': optional: true + '@esbuild/android-arm@0.27.4': + optional: true + '@esbuild/android-x64@0.18.20': optional: true @@ -8316,6 +8703,9 @@ snapshots: '@esbuild/android-x64@0.27.3': optional: true + '@esbuild/android-x64@0.27.4': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true @@ -8331,6 +8721,9 @@ snapshots: '@esbuild/darwin-arm64@0.27.3': optional: true + '@esbuild/darwin-arm64@0.27.4': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true @@ -8346,6 +8739,9 @@ snapshots: '@esbuild/darwin-x64@0.27.3': optional: true + '@esbuild/darwin-x64@0.27.4': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true @@ -8361,6 +8757,9 @@ snapshots: '@esbuild/freebsd-arm64@0.27.3': optional: true + '@esbuild/freebsd-arm64@0.27.4': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true @@ -8376,6 +8775,9 @@ snapshots: '@esbuild/freebsd-x64@0.27.3': optional: true + '@esbuild/freebsd-x64@0.27.4': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true @@ -8391,6 +8793,9 @@ snapshots: '@esbuild/linux-arm64@0.27.3': optional: true + '@esbuild/linux-arm64@0.27.4': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true @@ -8406,6 +8811,9 @@ snapshots: '@esbuild/linux-arm@0.27.3': optional: true + '@esbuild/linux-arm@0.27.4': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true @@ -8421,6 +8829,9 @@ snapshots: '@esbuild/linux-ia32@0.27.3': optional: true + '@esbuild/linux-ia32@0.27.4': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true @@ -8436,6 +8847,9 @@ snapshots: '@esbuild/linux-loong64@0.27.3': optional: true + '@esbuild/linux-loong64@0.27.4': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true @@ -8451,6 +8865,9 @@ snapshots: '@esbuild/linux-mips64el@0.27.3': optional: true + '@esbuild/linux-mips64el@0.27.4': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true @@ -8466,6 +8883,9 @@ snapshots: '@esbuild/linux-ppc64@0.27.3': optional: true + '@esbuild/linux-ppc64@0.27.4': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true @@ -8481,6 +8901,9 @@ snapshots: '@esbuild/linux-riscv64@0.27.3': optional: true + '@esbuild/linux-riscv64@0.27.4': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true @@ -8496,6 +8919,9 @@ snapshots: '@esbuild/linux-s390x@0.27.3': optional: true + '@esbuild/linux-s390x@0.27.4': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true @@ -8511,6 +8937,9 @@ snapshots: '@esbuild/linux-x64@0.27.3': optional: true + '@esbuild/linux-x64@0.27.4': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true @@ -8520,6 +8949,9 @@ snapshots: '@esbuild/netbsd-arm64@0.27.3': optional: true + '@esbuild/netbsd-arm64@0.27.4': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true @@ -8535,6 +8967,9 @@ snapshots: '@esbuild/netbsd-x64@0.27.3': optional: true + '@esbuild/netbsd-x64@0.27.4': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true @@ -8544,6 +8979,9 @@ snapshots: '@esbuild/openbsd-arm64@0.27.3': optional: true + '@esbuild/openbsd-arm64@0.27.4': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true @@ -8559,6 +8997,9 @@ snapshots: '@esbuild/openbsd-x64@0.27.3': optional: true + '@esbuild/openbsd-x64@0.27.4': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true @@ -8568,6 +9009,9 @@ snapshots: '@esbuild/openharmony-arm64@0.27.3': optional: true + '@esbuild/openharmony-arm64@0.27.4': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true @@ -8583,6 +9027,9 @@ snapshots: '@esbuild/sunos-x64@0.27.3': optional: true + '@esbuild/sunos-x64@0.27.4': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true @@ -8598,6 +9045,9 @@ snapshots: '@esbuild/win32-arm64@0.27.3': optional: true + '@esbuild/win32-arm64@0.27.4': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true @@ -8613,6 +9063,9 @@ snapshots: '@esbuild/win32-ia32@0.27.3': optional: true + '@esbuild/win32-ia32@0.27.4': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true @@ -8628,6 +9081,9 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@esbuild/win32-x64@0.27.4': + optional: true + '@fastify/busboy@2.1.1': {} '@grpc/grpc-js@1.14.3': @@ -8666,8 +9122,9 @@ snapshots: - bufferutil - utf-8-validate - '@hono/standard-validator@0.1.5(hono@4.12.2)': + '@hono/standard-validator@0.1.5(@standard-schema/spec@1.1.0)(hono@4.12.2)': dependencies: + '@standard-schema/spec': 1.1.0 hono: 4.12.2 '@hono/zod-openapi@1.2.2(hono@4.12.2)(zod@4.3.6)': @@ -8902,6 +9359,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@noble/ciphers@2.1.1': {} + + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9746,6 +10207,8 @@ snapshots: '@speed-highlight/core@1.2.14': {} + '@standard-schema/spec@1.1.0': {} + '@tanstack/history@1.161.4': {} '@tanstack/query-core@5.90.20': {} @@ -9923,7 +10386,7 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/node@25.4.0': + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 @@ -9991,7 +10454,7 @@ snapshots: - bare-abort-controller - react-native-b4a - '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.4.0))': + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.5.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -9999,11 +10462,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21(@types/node@25.4.0) + vite: 5.4.21(@types/node@25.5.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -10011,11 +10474,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -10023,7 +10486,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -10051,13 +10514,13 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@24.10.9) - '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.4.0))': + '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.5.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@25.4.0) + vite: 5.4.21(@types/node@25.5.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -10149,7 +10612,7 @@ snapshots: assertion-error@2.0.1: {} - astro@5.16.15(@types/node@25.4.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + astro@5.16.15(@types/node@25.5.0)(aws4fetch@1.0.20)(jiti@1.21.7)(rollup@4.56.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -10206,8 +10669,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.4(aws4fetch@1.0.20) vfile: 6.0.3 - vite: 6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) + vite: 6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -10342,6 +10805,45 @@ snapshots: dependencies: tweetnacl: 0.14.5 + better-auth@1.5.5(@cloudflare/workers-types@4.20260313.1)(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0))(pg@8.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0)) + '@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/prisma-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/telemetry': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260313.1)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.3.2(zod@4.3.6) + defu: 6.1.4 + jose: 6.1.3 + kysely: 0.28.11 + nanostores: 1.1.1 + zod: 4.3.6 + optionalDependencies: + drizzle-kit: 0.31.9 + drizzle-orm: 0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0) + pg: 8.20.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + solid-js: 1.9.11 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - '@cloudflare/workers-types' + + better-call@1.3.2(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.0.1 + optionalDependencies: + zod: 4.3.6 + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 @@ -10577,7 +11079,7 @@ snapshots: compare-versions@6.1.1: {} - computesdk@2.3.0: + computesdk@2.5.0: dependencies: '@computesdk/cmd': 0.4.1 @@ -10933,14 +11435,15 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260310.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(pg@8.20.0): + drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0): optionalDependencies: - '@cloudflare/workers-types': 4.20260310.1 + '@cloudflare/workers-types': 4.20260313.1 '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.18.0 better-sqlite3: 11.10.0 bun-types: 1.3.10 + kysely: 0.28.11 pg: 8.20.0 dset@3.1.4: {} @@ -11152,6 +11655,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -11725,6 +12257,8 @@ snapshots: kleur@4.1.5: {} + kysely@0.28.11: {} + lefthook-darwin-arm64@2.1.3: optional: true @@ -12198,12 +12732,12 @@ snapshots: mimic-response@3.1.0: {} - miniflare@4.20260310.0: + miniflare@4.20260312.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 undici: 7.18.2 - workerd: 1.20260310.1 + workerd: 1.20260312.1 ws: 8.18.0 youch: 4.1.0-beta.10 transitivePeerDependencies: @@ -12271,6 +12805,8 @@ snapshots: nanoid@3.3.11: {} + nanostores@1.1.1: {} + napi-build-utils@2.0.0: {} negotiator@1.0.0: {} @@ -13015,9 +13551,9 @@ snapshots: reusify@1.1.0: {} - rivetkit@2.1.6(@hono/node-server@1.19.9(hono@4.12.2))(@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2))(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260310.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(pg@8.20.0))(ws@8.19.0): + rivetkit@2.1.6(@hono/node-server@1.19.9(hono@4.12.2))(@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2))(@standard-schema/spec@1.1.0)(drizzle-kit@0.31.9)(drizzle-orm@0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0))(ws@8.19.0): dependencies: - '@hono/standard-validator': 0.1.5(hono@4.12.2) + '@hono/standard-validator': 0.1.5(@standard-schema/spec@1.1.0)(hono@4.12.2) '@hono/zod-openapi': 1.2.2(hono@4.12.2)(zod@4.3.6) '@rivetkit/bare-ts': 0.6.2 '@rivetkit/engine-runner': 2.1.6 @@ -13043,7 +13579,7 @@ snapshots: '@hono/node-server': 1.19.9(hono@4.12.2) '@hono/node-ws': 1.3.0(@hono/node-server@1.19.9(hono@4.12.2))(hono@4.12.2) drizzle-kit: 0.31.9 - drizzle-orm: 0.44.7(@cloudflare/workers-types@4.20260310.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(pg@8.20.0) + drizzle-orm: 0.44.7(@cloudflare/workers-types@4.20260313.1)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.18.0)(better-sqlite3@11.10.0)(bun-types@1.3.10)(kysely@0.28.11)(pg@8.20.0) ws: 8.19.0 transitivePeerDependencies: - '@standard-schema/spec' @@ -13083,6 +13619,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.56.0 fsevents: 2.3.3 + rou3@0.7.12: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -13148,6 +13686,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@3.0.1: {} + setprototypeof@1.2.0: {} sharp@0.34.5: @@ -13811,13 +14351,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@22.19.7)(jiti@1.21.7)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.10.9)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@22.19.7)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.9)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -13832,55 +14372,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.10.9)(jiti@1.21.7)(yaml@2.8.2): + vite-node@3.2.4(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@24.10.9)(jiti@1.21.7)(yaml@2.8.2) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.2.4(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.2.4(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.1(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -13913,16 +14411,16 @@ snapshots: '@types/node': 24.10.9 fsevents: 2.3.3 - vite@5.4.21(@types/node@25.4.0): + vite@5.4.21(@types/node@25.5.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.56.0 optionalDependencies: - '@types/node': 25.4.0 + '@types/node': 25.5.0 fsevents: 2.3.3 - vite@6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -13931,7 +14429,7 @@ snapshots: rollup: 4.56.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.4.0 + '@types/node': 25.5.0 fsevents: 2.3.3 jiti: 1.21.7 tsx: 4.21.0 @@ -13952,21 +14450,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(yaml@2.8.2): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.56.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.19.7 - fsevents: 2.3.3 - jiti: 1.21.7 - yaml: 2.8.2 - - vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(yaml@2.8.2): + vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13978,9 +14462,10 @@ snapshots: '@types/node': 24.10.9 fsevents: 2.3.3 jiti: 1.21.7 + tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13989,29 +14474,15 @@ snapshots: rollup: 4.56.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.4.0 + '@types/node': 25.5.0 fsevents: 2.3.3 jiti: 1.21.7 tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.56.0 - tinyglobby: 0.2.15 + vitefu@1.1.1(vite@6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: - '@types/node': 25.4.0 - fsevents: 2.3.3 - jiti: 1.21.7 - yaml: 2.8.2 - - vitefu@1.1.1(vite@6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)): - optionalDependencies: - vite: 6.4.1(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: @@ -14055,49 +14526,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(yaml@2.8.2): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@22.19.7)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@22.19.7) - vite-node: 3.2.4(@types/node@22.19.7)(jiti@1.21.7)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 22.19.7 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@1.21.7)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -14120,7 +14549,7 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 5.4.21(@types/node@24.10.9) - vite-node: 3.2.4(@types/node@24.10.9)(jiti@1.21.7)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.9)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -14139,11 +14568,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.4.0)) + '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.5.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -14161,54 +14590,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@25.4.0) - vite-node: 3.2.4(@types/node@25.4.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) + vite: 5.4.21(@types/node@25.5.0) + vite-node: 3.2.4(@types/node@25.5.0)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.4.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.4.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@25.4.0) - vite-node: 3.2.4(@types/node@25.4.0)(jiti@1.21.7)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 25.4.0 + '@types/node': 25.5.0 transitivePeerDependencies: - jiti - less @@ -14254,26 +14641,26 @@ snapshots: dependencies: string-width: 7.2.0 - workerd@1.20260310.1: + workerd@1.20260312.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260310.1 - '@cloudflare/workerd-darwin-arm64': 1.20260310.1 - '@cloudflare/workerd-linux-64': 1.20260310.1 - '@cloudflare/workerd-linux-arm64': 1.20260310.1 - '@cloudflare/workerd-windows-64': 1.20260310.1 + '@cloudflare/workerd-darwin-64': 1.20260312.1 + '@cloudflare/workerd-darwin-arm64': 1.20260312.1 + '@cloudflare/workerd-linux-64': 1.20260312.1 + '@cloudflare/workerd-linux-arm64': 1.20260312.1 + '@cloudflare/workerd-windows-64': 1.20260312.1 - wrangler@4.72.0(@cloudflare/workers-types@4.20260310.1): + wrangler@4.73.0(@cloudflare/workers-types@4.20260313.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260310.1) + '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260312.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260310.0 + miniflare: 4.20260312.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260310.1 + workerd: 1.20260312.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260310.1 + '@cloudflare/workers-types': 4.20260313.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil