Auth flow refactor, project requests, and routing updates

- Add server-side auth token loader (auth.server.ts) with SSR cookie forwarding
- Add project-requests client for explicit/signal/existing issue dispatch
- Refactor auth layout and app layout for SSR auth gating
- Update auth-client and auth-provider for convex token flow
- Extend project-issue primitives with request result schema
- Wire projectIssues backend mutation for request handling
- Update slice-one page and project-workspace hook
- Adjust routes (remove unused), Caddy config, and env templates
This commit is contained in:
Miniputer
2026-07-27 18:47:43 +05:30
parent cc47007fa9
commit 1855735245
24 changed files with 340 additions and 108 deletions

View File

@@ -1,6 +1,5 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineAgent } from "@flue/runtime";
import type { AgentRouteHandler } from "@flue/runtime";
import { createFinalizeGiteaLifecycle } from "../actions/finalize-gitea-lifecycle";
import { agentOs } from "../sandboxes/agent-os";
@@ -9,8 +8,6 @@ import { createProjectTools } from "../tools/project";
export const description =
"Works one repository issue inside an isolated AgentOS workspace.";
export const route: AgentRouteHandler = (_context, next) => next();
export default defineAgent(({ env, id }) => {
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);

View File

@@ -31,6 +31,11 @@ const createIssueFromSignal = makeFunctionReference<
{ readonly signalId: string },
{ readonly issueId: string; readonly projectId: string }
>("projectIssues:createFromSignal");
const getIssue = makeFunctionReference<
"query",
{ readonly issueId: string },
{ readonly issueId: string; readonly projectId: string } | null
>("projectIssues:getForDispatch");
const beginIssue = makeFunctionReference<
"mutation",
@@ -75,6 +80,14 @@ const createIssueForRequest = async (
client: ConvexHttpClient,
request: Awaited<ReturnType<typeof decodeRequest>>
): Promise<{ readonly issueId: string; readonly projectId: string }> => {
if (request.kind === "existing") {
const issue = await client.query(getIssue, { issueId: request.issueId });
if (!issue) {
throw new Error("Project issue not found");
}
return issue;
}
if (request.kind === "signal") {
return client.mutation(createIssueFromSignal, {
signalId: request.signalId,

View File

@@ -1,8 +1,8 @@
import { env } from "@code/env/web";
import { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: env.VITE_CONVEX_SITE_URL,
plugins: [convexClient(), crossDomainClient()],
baseURL: env.VITE_AUTH_URL,
plugins: [convexClient()],
});

View File

@@ -7,8 +7,18 @@ import { authClient } from "./auth-client";
const convex = new ConvexReactClient(env.VITE_CONVEX_URL, { expectAuth: true });
export const WebAuthProvider = ({ children }: { children: ReactNode }) => (
<ConvexBetterAuthProvider authClient={authClient} client={convex}>
export const WebAuthProvider = ({
children,
initialToken,
}: {
readonly children: ReactNode;
readonly initialToken?: string | null;
}) => (
<ConvexBetterAuthProvider
authClient={authClient}
client={convex}
initialToken={initialToken}
>
{children}
</ConvexBetterAuthProvider>
);

View File

@@ -2,7 +2,7 @@ import { expo } from "@better-auth/expo";
import { env } from "@code/env/convex";
import { createClient } from "@convex-dev/better-auth";
import type { GenericCtx } from "@convex-dev/better-auth";
import { convex, crossDomain } from "@convex-dev/better-auth/plugins";
import { convex } from "@convex-dev/better-auth/plugins";
import { betterAuth } from "better-auth/minimal";
import { components } from "./_generated/api";
@@ -25,7 +25,6 @@ const createAuth = (ctx: GenericCtx<DataModel>) =>
},
plugins: [
expo(),
crossDomain({ siteUrl }),
convex({
authConfig,
jwksRotateOnTokenGenerationError: true,

View File

@@ -118,6 +118,18 @@ export const createFromSignal = mutation({
},
});
export const getForDispatch = query({
args: { issueId: v.id("projectIssues") },
handler: async (ctx, args) => {
const issue = await ctx.db.get("projectIssues", args.issueId);
if (!issue) {
return null;
}
await requireProjectMember(ctx, issue.projectId);
return { issueId: issue._id, projectId: issue.projectId };
},
});
export const begin = mutation({
args: { issueId: v.id("projectIssues") },
handler: async (ctx, args) => {

View File

@@ -1,6 +1,6 @@
interface ImportMetaEnv {
readonly [key: string]: string | number | boolean | undefined;
readonly VITE_CONVEX_SITE_URL: string;
readonly VITE_AUTH_URL: string;
readonly VITE_FLUE_URL: string;
readonly VITE_CONVEX_URL: string;
}

View File

@@ -8,10 +8,9 @@ const convexUrlSchema = (exampleHost: string) =>
export const env = createEnv({
client: {
VITE_CONVEX_SITE_URL: convexUrlSchema("example.convex.site"),
VITE_AUTH_URL: z.url(),
VITE_CONVEX_URL: convexUrlSchema("example.convex.cloud"),
VITE_FLUE_URL: z.url(),
VITE_ZOPU_SERVER_URL: z.url(),
},
clientPrefix: "VITE_",
emptyStringAsUndefined: true,

View File

@@ -29,6 +29,17 @@ describe("project issue primitives", () => {
});
});
it("accepts an existing issue request without creating a duplicate", () => {
const request = Effect.runSync(
decodeProjectIssueRequest({
issueId: "issue-1",
kind: "existing",
})
);
expect(request).toEqual({ issueId: "issue-1", kind: "existing" });
});
it("rejects invalid issue drafts before persistence", () => {
const error = Effect.runSync(
Effect.flip(

View File

@@ -35,6 +35,11 @@ export const ProjectIssueDraft = Schema.Struct({
});
export type ProjectIssueDraft = typeof ProjectIssueDraft.Type;
export const ExistingProjectIssueRequest = Schema.Struct({
issueId: ProjectIssueId,
kind: Schema.Literal("existing"),
});
export const ExplicitProjectIssueRequest = Schema.Struct({
body: Schema.String,
kind: Schema.Literal("explicit"),
@@ -48,6 +53,7 @@ export const SignalProjectIssueRequest = Schema.Struct({
});
export const ProjectIssueRequest = Schema.Union([
ExistingProjectIssueRequest,
ExplicitProjectIssueRequest,
SignalProjectIssueRequest,
]);
@@ -186,7 +192,7 @@ export const decodeProjectIssueRequest = (
() =>
new ProjectIssueRequestError({
message:
"Use a signal request or an explicit request with projectId, title, and body",
"Use an existing issue, signal request, or explicit request with projectId, title, and body",
reason: "InvalidInput",
})
)