feat: consolidate web auth and add Effect v4 Project primitives

Auth:
- Add useWebAuth hook as the single web-facing auth interface with four
  states (loading/unauthenticated/authenticated/inconsistent)
- Normalize getCurrentUser to return {id: tokenIdentifier, name, email}
- Rename requireOwnerId to requireAuthUserId across all backend callers
- Set expectAuth:true on ConvexReactClient singleton
- Migrate _app, _auth, user-menu, use-personal-organization to useWebAuth
- Remove unused @code/auth/server export (zero callers confirmed)

Primitives:
- Add packages/primitives/src/project.ts with Effect v4 Project domain:
  URL normalization, six canonical context kinds, strict remote decoding,
  context write decisioning with revision tracking, and no-op detection
- Define PublicGit and ProjectStore replaceable ports
- Define ProjectApplication service orchestrating domain+ports with
  deterministic error mapping
- Add comprehensive test suite (28 tests) with in-memory store and fake
  PublicGit covering normalization, seeds, idempotency, tenant isolation,
  last-write-wins, and typed error mapping
This commit is contained in:
-Puter
2026-07-23 18:04:02 +05:30
parent ab9426b8fc
commit 477e54240d
20 changed files with 1827 additions and 97 deletions

View File

@@ -3,7 +3,7 @@ import { ConvexError, v } from "convex/values";
import type { Id } from "./_generated/dataModel";
import { mutation, query } from "./_generated/server";
import type { MutationCtx } from "./_generated/server";
import { requireOwnerId } from "./authz";
import { requireAuthUserId } from "./authz"
const requireProjectOwner = async (
ctx: MutationCtx,
@@ -24,7 +24,7 @@ export const create = mutation({
title: v.string(),
},
handler: async (ctx, args) => {
const ownerId = await requireOwnerId(ctx);
const ownerId = await requireAuthUserId(ctx);
await requireProjectOwner(ctx, args.projectId, ownerId);
const title = args.title.trim();
const body = args.body.trim();
@@ -81,7 +81,7 @@ export const create = mutation({
export const begin = mutation({
args: { issueId: v.id("projectIssues") },
handler: async (ctx, args) => {
const ownerId = await requireOwnerId(ctx);
const ownerId = await requireAuthUserId(ctx);
const issue = await ctx.db.get("projectIssues", args.issueId);
if (!issue) {
throw new ConvexError("Issue not found");
@@ -109,7 +109,7 @@ export const begin = mutation({
export const markDispatchFailed = mutation({
args: { error: v.string(), issueId: v.id("projectIssues") },
handler: async (ctx, args) => {
const ownerId = await requireOwnerId(ctx);
const ownerId = await requireAuthUserId(ctx);
const issue = await ctx.db.get("projectIssues", args.issueId);
if (!issue) {
throw new ConvexError("Issue not found");
@@ -133,7 +133,7 @@ export const markDispatchFailed = mutation({
export const list = query({
args: { projectId: v.id("projects") },
handler: async (ctx, args) => {
const ownerId = await requireOwnerId(ctx);
const ownerId = await requireAuthUserId(ctx);
const project = await ctx.db.get("projects", args.projectId);
if (!project || project.ownerId !== ownerId) {
return [];