Effect primitives: - git-provider: GitProvider, connection states, normalized errors, URL normalization, host compatibility, credential freshness window - git-provisioning: validated Puter commands, migration states, idempotency keys, safe replacement rules, owner-safe guards - git-webhook: supported events, signature verification, delivery states - host-repository: provider-neutral credential-safe clone via GIT_ASKPASS Normalized Convex schema: - gitProviderAccounts, refined gitConnections, gitProviderOrganizations, gitRepositories, gitMigrations, gitWebhookDeliveries - projects: gitRepositoryId + instructions fields - Schema fields optional for backward compatibility, with backfill cron Backend: - Connection health: verify action, hourly reconciliation (covers stale active + reauth-required + undefined-state legacy connections) - Puter provisioning: createPuterUser/Organization/Repository with owner binding, startGithubMigration (durable via scheduler), getMigration - Org ownership: explicit member add with admin role + verification - Webhook HTTP actions: HMAC verification, delivery persistence with idempotency, repository resolution, byte-length payload limit - Automatic Puter webhook creation after repo creation/migration with fail-loud state tracking - Repository sync after connection (Gitea + GitHub) - AgentOS execution resolves gitRepositoryId for real clone URL - Credential gating: state + freshness checks before execution and project creation - listForOrganization for cross-project Work filtering Frontend: - /projects onboarding page with GitHub OAuth (linkSocial) and Puter PAT - Zero-project redirect, repository selection, context editor - Provider-aware settings panel (no serverUrl for Puter) - Project selection via ?project= query param - GitHub scopes: repo + read:org Agent runtime: - Clones user repository with GIT_ASKPASS credential helper (no token in URL, args, or git config), provider-aware username - Removed fixed Zopu source path and .env copy
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
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 } from "@convex-dev/better-auth/plugins";
|
|
import { betterAuth } from "better-auth/minimal";
|
|
|
|
import { components } from "./_generated/api";
|
|
import type { DataModel } from "./_generated/dataModel";
|
|
import { query } from "./_generated/server";
|
|
import authConfig from "./auth.config";
|
|
|
|
const siteUrl = env.SITE_URL;
|
|
const nativeAppUrl = env.NATIVE_APP_URL ?? "code://";
|
|
|
|
export const authComponent = createClient<DataModel>(components.betterAuth);
|
|
|
|
const createAuth = (ctx: GenericCtx<DataModel>) =>
|
|
betterAuth({
|
|
advanced: { useSecureCookies: siteUrl.startsWith("https://") },
|
|
baseURL: env.CONVEX_SITE_URL,
|
|
database: authComponent.adapter(ctx),
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
requireEmailVerification: false,
|
|
},
|
|
plugins: [
|
|
expo(),
|
|
convex({
|
|
authConfig,
|
|
jwksRotateOnTokenGenerationError: true,
|
|
}),
|
|
],
|
|
socialProviders:
|
|
env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET
|
|
? {
|
|
github: {
|
|
clientId: env.GITHUB_CLIENT_ID,
|
|
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
scope: ["repo", "read:org"],
|
|
},
|
|
}
|
|
: {},
|
|
trustedOrigins: [siteUrl, nativeAppUrl, "exp://"],
|
|
});
|
|
|
|
export { createAuth };
|
|
|
|
export interface AuthUserView {
|
|
readonly id: string;
|
|
readonly name: string;
|
|
readonly email: string;
|
|
}
|
|
|
|
/**
|
|
* Resolve the authenticated user to the canonical web view. The returned `id`
|
|
* is the Convex `identity.tokenIdentifier`, the stable authenticated identity
|
|
* used across organization/conversation/Signal ownership. Returns null when no
|
|
* identity is present or the Better Auth user row cannot be resolved.
|
|
*/
|
|
export const getCurrentUser = query({
|
|
args: {},
|
|
handler: async (ctx): Promise<AuthUserView | null> => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) {
|
|
return null;
|
|
}
|
|
const authUser = await authComponent.safeGetAuthUser(ctx);
|
|
if (!authUser) {
|
|
return null;
|
|
}
|
|
return {
|
|
email: authUser.email,
|
|
id: identity.tokenIdentifier,
|
|
name: authUser.name,
|
|
};
|
|
},
|
|
});
|