From 165e8fb35e7f33e79d3cdf39a46d14aa63afdf24 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 14:31:32 +0200 Subject: [PATCH 01/14] feat: sync convex better auth runtime fixes --- .agents/rules/sync-convex-auth.mdc | 270 +++++++++++++++++ .agents/skills/sync-convex-auth/SKILL.md | 274 ++++++++++++++++++ .changeset/convex-135-agentic-bootstrap.md | 1 + .claude/skills/sync-convex-auth | 1 + bun.lock | 6 +- ...th-upstream-sync-runtime-fixes-20260416.md | 119 ++++++++ package.json | 2 +- packages/kitcn/package.json | 2 +- .../skills/convex/references/features/auth.md | 21 ++ .../skills/convex/references/setup/auth.md | 3 +- packages/kitcn/src/auth-http/index.ts | 2 +- packages/kitcn/src/auth/adapter-utils.test.ts | 85 ++++++ packages/kitcn/src/auth/adapter-utils.ts | 8 +- packages/kitcn/src/auth/auth-config.ts | 2 +- .../kitcn/src/auth/registerRoutes.test.ts | 63 +++- packages/kitcn/src/auth/registerRoutes.ts | 92 ++++-- .../kitcn/src/auth/registerRoutes.types.ts | 7 +- www/content/docs/auth/server.mdx | 6 +- 18 files changed, 929 insertions(+), 35 deletions(-) create mode 100644 .agents/rules/sync-convex-auth.mdc create mode 100644 .agents/skills/sync-convex-auth/SKILL.md create mode 120000 .claude/skills/sync-convex-auth create mode 100644 docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md diff --git a/.agents/rules/sync-convex-auth.mdc b/.agents/rules/sync-convex-auth.mdc new file mode 100644 index 00000000..c7ff42c6 --- /dev/null +++ b/.agents/rules/sync-convex-auth.mdc @@ -0,0 +1,270 @@ +--- +description: Sync kitcn against upstream `convex-better-auth` changes. Use when asked to run `sync-convex-auth`, compare `zbeyens/convex-better-auth` with its upstream fork, audit commits the fork is behind on, classify relevance to kitcn auth integration, and delegate one implementation PR through `task`. +--- + +# Sync Convex Auth + +Handle $ARGUMENTS. + +Goal: compare `https://github.com/zbeyens/convex-better-auth` with its +upstream fork, extract every upstream change that matters to kitcn, then +delegate one coherent implementation slice to +[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) so it +opens the PR. + +## Rules + +- Use evidence, not vibes. Read commits, changed files, and patches. +- Treat the fork being behind upstream as signal, not proof. Pull only relevant + work into kitcn. +- Ignore genuinely irrelevant upstream changes. Do not mirror upstream just to + feel caught up. +- Pull all clearly relevant, non-conflicting fixes in the same slice when they + share the same kitcn auth surface. +- Stop and ask the user before importing optional additions where the tradeoff is + unclear, especially slow e2e suites, broad fixture rewrites, examples, + release plumbing, or dev-only test infrastructure. +- Prefer deleting kitcn glue over adding more glue when upstream fixed the real + problem. +- If no actionable opportunity exists, stop with the evidence. Do not open a + vanity PR. + +## 1. Establish Fork, Upstream, And Refs + +Use GitHub metadata to discover the upstream parent instead of guessing: + +```bash +gh repo view zbeyens/convex-better-auth \ + --json nameWithOwner,parent,defaultBranchRef \ + --jq '{fork: .nameWithOwner, parent: .parent.nameWithOwner, branch: .defaultBranchRef.name}' +``` + +If `parent` is missing or ambiguous, stop and ask for the upstream repo. + +Use a local clone for navigation, creating it only if missing: + +```bash +test -d ../convex-better-auth/.git || \ + gh repo clone zbeyens/convex-better-auth ../convex-better-auth +git -C ../convex-better-auth remote get-url upstream >/dev/null 2>&1 || \ + git -C ../convex-better-auth remote add upstream https://github.com//.git +git -C ../convex-better-auth fetch origin --tags +git -C ../convex-better-auth fetch upstream --tags +``` + +Record: + +- fork owner/name and default branch +- upstream owner/name and default branch +- fork ref and upstream ref being compared +- behind count +- ahead count, if any +- exact commit range + +Commands: + +```bash +git -C ../convex-better-auth rev-list --count origin/..upstream/ +git -C ../convex-better-auth rev-list --count upstream/..origin/ +git -C ../convex-better-auth log --oneline --decorate origin/..upstream/ +``` + +If $ARGUMENTS names a base or target ref, use it as the bound after proving it +exists. + +## 2. Read The Upstream Diff + +Start with a file summary: + +```bash +git -C ../convex-better-auth diff --name-status \ + origin/..upstream/ +``` + +Then read patches for relevant-looking files: + +```bash +git -C ../convex-better-auth diff \ + origin/..upstream/ -- \ + src package.json bun.lock tsconfig.json '*.md' \ + ':!**/dist/**' ':!**/build/**' ':!**/node_modules/**' +``` + +Use `gh` compare when it gives cleaner commit/file metadata: + +```bash +gh api \ + repos///compare/:... \ + --jq '.commits[] | {sha: .sha, message: .commit.message}' + +gh api \ + repos///compare/:... \ + --jq '.files[] | {filename,status,patch}' +``` + +If the compare is too large, group by subsystem first, then inspect the patches +for likely auth-runtime impact. + +## 3. Search Kitcn For Affected Auth Surfaces + +Search local kitcn integration points: + +```bash +rg -n "@convex-dev/better-auth|convexBetterAuth|getToken|convexClient|convex\\(|BetterAuth|better-auth|auth" \ + packages www .agents docs tooling fixtures example +``` + +Search institutional notes before proposing work: + +```bash +rg -i --files-with-matches \ + "convex-better-auth|@convex-dev/better-auth|better-auth|auth|react-start|nextjs|jwt|jwks|session|cookie|schema|plugin|getToken" \ + docs/solutions docs/plans +``` + +Read relevant hits, especially notes about: + +- `@convex-dev/better-auth` reexports and wrappers +- `kitcn/auth`, `kitcn/auth-client`, `kitcn/auth-nextjs`, and + `kitcn/auth-start` +- Better Auth and Convex version compatibility +- token, JWT, JWKS, cookie, and session handling +- schema generation, plugin reconciliation, and generated auth contracts +- React, Solid, Next.js, and TanStack Start provider behavior +- scaffold templates, docs, and `packages/kitcn/skills/convex/**` +- local hacks that might be obsolete after upstream changes + +## 4. Classify Every Upstream Change + +Classify each commit or file group: + +- `compatibility`: required work to keep kitcn working with upstream auth, + Better Auth, Convex, framework, or package changes. +- `security`: auth correctness or security hardening kitcn should not miss. +- `bugfix`: upstream fix that maps to a kitcn runtime, provider, token, schema, + routing, or scaffold issue. +- `feature`: new upstream API, helper, framework support, or auth capability + kitcn can expose cleanly. +- `cleanup`: upstream change that lets kitcn delete a workaround, wrapper, + fallback, doc warning, copied logic, or special-case patch. +- `docs`: upstream change that only affects user-facing docs, setup guidance, or + skills. +- `tests`: upstream test coverage or harness changes. +- `no-op`: interesting upstream change with no kitcn action. + +For every non-`no-op`, include: + +- commit evidence +- diff evidence +- local kitcn files affected +- expected implementation surface +- verification command(s) +- confidence + +Use this relevance filter: + +- Relevant: runtime auth behavior, package exports kitcn imports or reexports, + helpers kitcn wraps, version compatibility, security, framework integration, + schema/plugin behavior, generated code contracts, docs/skills users rely on, + and cleanup of known kitcn workarounds. +- Usually irrelevant: upstream release config, repository-only CI, maintainer + docs, benchmark harnesses, examples that do not map to kitcn scaffolds, and + tests for behavior kitcn neither exposes nor depends on. +- Ambiguous optional: added test suites, e2e harnesses, examples, fixtures, + benchmark tooling, and dev-only utilities. Stop and ask before pulling these + in unless they are the direct verification path for a selected required fix. + +## 5. Choose One Implementation Slice + +Pick the highest-leverage slice using this order: + +1. security fix +2. compatibility breakage +3. bugfix that affects kitcn users +4. delete dirty hack made obsolete upstream +5. agentic or DX improvement for deterministic setup, CLI, or generated output +6. feature kitcn can expose cleanly +7. docs or skill-only update +8. optional tests or examples only after user approval + +If several relevant upstream fixes touch the same auth surface and do not +conflict, delegate them together. If they touch separate surfaces, pick the +highest-risk slice first. + +If the winning slice touches published package code, the delegated task must +update the active changeset and run `bun --cwd packages/kitcn build`. + +If it touches scaffold templates, the delegated task must run +`bun run fixtures:sync` and `bun run fixtures:check`. + +If it touches auth runtime, client, provider, or query invalidation surfaces, +the delegated task must follow the repo's auth verification lane. Do not import +a slow upstream e2e suite unless the user explicitly approves it. + +## 6. Delegate Through `task` + +Load +[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) with a +prompt in this exact shape: + +```md +Implement this convex-better-auth sync opportunity. + +Fork: zbeyens/convex-better-auth +Upstream: / +Range: .. +Behind: commits + +Opportunity: +Class: + +Evidence: +- Upstream commits: +- Upstream diff: +- Kitcn evidence: + +Implementation: +- +- +- + +Acceptance: +- +- +- +- open the PR after verification + +Do not preserve obsolete auth workarounds if the upstream change removes the +need for them. Hard cut the hack. +Do not add optional slow e2e suites, broad examples, or dev-only upstream test +infrastructure unless the user approved that scope. +``` + +Then follow `task` until the PR exists or a real blocker is proven. + +## Output + +Before delegation, keep the audit terse: + +```md +Fork: zbeyens/convex-better-auth +Upstream: / +Range: .. +Behind: + +| Class | Opportunity | Evidence | Decision | +| --- | --- | --- | --- | +| bugfix | ... | ... | selected | + +Delegating to task: +``` + +If the right choice is ambiguous, stop and ask one pointed question. Example: + +```md +Upstream added a Playwright e2e suite that does not fix a current kitcn bug. +Do you want that pulled in, or should I ignore it and keep this sync to runtime +fixes only? +``` + +After `task` finishes, use its final handoff format. diff --git a/.agents/skills/sync-convex-auth/SKILL.md b/.agents/skills/sync-convex-auth/SKILL.md new file mode 100644 index 00000000..99f20005 --- /dev/null +++ b/.agents/skills/sync-convex-auth/SKILL.md @@ -0,0 +1,274 @@ +--- +description: Sync kitcn against upstream `convex-better-auth` changes. Use when asked to run `sync-convex-auth`, compare `zbeyens/convex-better-auth` with its upstream fork, audit commits the fork is behind on, classify relevance to kitcn auth integration, and delegate one implementation PR through `task`. +name: sync-convex-auth +metadata: + skiller: + source: .agents/rules/sync-convex-auth.mdc +--- + +# Sync Convex Auth + +Handle $ARGUMENTS. + +Goal: compare `https://github.com/zbeyens/convex-better-auth` with its +upstream fork, extract every upstream change that matters to kitcn, then +delegate one coherent implementation slice to +[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) so it +opens the PR. + +## Rules + +- Use evidence, not vibes. Read commits, changed files, and patches. +- Treat the fork being behind upstream as signal, not proof. Pull only relevant + work into kitcn. +- Ignore genuinely irrelevant upstream changes. Do not mirror upstream just to + feel caught up. +- Pull all clearly relevant, non-conflicting fixes in the same slice when they + share the same kitcn auth surface. +- Stop and ask the user before importing optional additions where the tradeoff is + unclear, especially slow e2e suites, broad fixture rewrites, examples, + release plumbing, or dev-only test infrastructure. +- Prefer deleting kitcn glue over adding more glue when upstream fixed the real + problem. +- If no actionable opportunity exists, stop with the evidence. Do not open a + vanity PR. + +## 1. Establish Fork, Upstream, And Refs + +Use GitHub metadata to discover the upstream parent instead of guessing: + +```bash +gh repo view zbeyens/convex-better-auth \ + --json nameWithOwner,parent,defaultBranchRef \ + --jq '{fork: .nameWithOwner, parent: .parent.nameWithOwner, branch: .defaultBranchRef.name}' +``` + +If `parent` is missing or ambiguous, stop and ask for the upstream repo. + +Use a local clone for navigation, creating it only if missing: + +```bash +test -d ../convex-better-auth/.git || \ + gh repo clone zbeyens/convex-better-auth ../convex-better-auth +git -C ../convex-better-auth remote get-url upstream >/dev/null 2>&1 || \ + git -C ../convex-better-auth remote add upstream https://github.com//.git +git -C ../convex-better-auth fetch origin --tags +git -C ../convex-better-auth fetch upstream --tags +``` + +Record: + +- fork owner/name and default branch +- upstream owner/name and default branch +- fork ref and upstream ref being compared +- behind count +- ahead count, if any +- exact commit range + +Commands: + +```bash +git -C ../convex-better-auth rev-list --count origin/..upstream/ +git -C ../convex-better-auth rev-list --count upstream/..origin/ +git -C ../convex-better-auth log --oneline --decorate origin/..upstream/ +``` + +If $ARGUMENTS names a base or target ref, use it as the bound after proving it +exists. + +## 2. Read The Upstream Diff + +Start with a file summary: + +```bash +git -C ../convex-better-auth diff --name-status \ + origin/..upstream/ +``` + +Then read patches for relevant-looking files: + +```bash +git -C ../convex-better-auth diff \ + origin/..upstream/ -- \ + src package.json bun.lock tsconfig.json '*.md' \ + ':!**/dist/**' ':!**/build/**' ':!**/node_modules/**' +``` + +Use `gh` compare when it gives cleaner commit/file metadata: + +```bash +gh api \ + repos///compare/:... \ + --jq '.commits[] | {sha: .sha, message: .commit.message}' + +gh api \ + repos///compare/:... \ + --jq '.files[] | {filename,status,patch}' +``` + +If the compare is too large, group by subsystem first, then inspect the patches +for likely auth-runtime impact. + +## 3. Search Kitcn For Affected Auth Surfaces + +Search local kitcn integration points: + +```bash +rg -n "@convex-dev/better-auth|convexBetterAuth|getToken|convexClient|convex\\(|BetterAuth|better-auth|auth" \ + packages www .agents docs tooling fixtures example +``` + +Search institutional notes before proposing work: + +```bash +rg -i --files-with-matches \ + "convex-better-auth|@convex-dev/better-auth|better-auth|auth|react-start|nextjs|jwt|jwks|session|cookie|schema|plugin|getToken" \ + docs/solutions docs/plans +``` + +Read relevant hits, especially notes about: + +- `@convex-dev/better-auth` reexports and wrappers +- `kitcn/auth`, `kitcn/auth-client`, `kitcn/auth-nextjs`, and + `kitcn/auth-start` +- Better Auth and Convex version compatibility +- token, JWT, JWKS, cookie, and session handling +- schema generation, plugin reconciliation, and generated auth contracts +- React, Solid, Next.js, and TanStack Start provider behavior +- scaffold templates, docs, and `packages/kitcn/skills/convex/**` +- local hacks that might be obsolete after upstream changes + +## 4. Classify Every Upstream Change + +Classify each commit or file group: + +- `compatibility`: required work to keep kitcn working with upstream auth, + Better Auth, Convex, framework, or package changes. +- `security`: auth correctness or security hardening kitcn should not miss. +- `bugfix`: upstream fix that maps to a kitcn runtime, provider, token, schema, + routing, or scaffold issue. +- `feature`: new upstream API, helper, framework support, or auth capability + kitcn can expose cleanly. +- `cleanup`: upstream change that lets kitcn delete a workaround, wrapper, + fallback, doc warning, copied logic, or special-case patch. +- `docs`: upstream change that only affects user-facing docs, setup guidance, or + skills. +- `tests`: upstream test coverage or harness changes. +- `no-op`: interesting upstream change with no kitcn action. + +For every non-`no-op`, include: + +- commit evidence +- diff evidence +- local kitcn files affected +- expected implementation surface +- verification command(s) +- confidence + +Use this relevance filter: + +- Relevant: runtime auth behavior, package exports kitcn imports or reexports, + helpers kitcn wraps, version compatibility, security, framework integration, + schema/plugin behavior, generated code contracts, docs/skills users rely on, + and cleanup of known kitcn workarounds. +- Usually irrelevant: upstream release config, repository-only CI, maintainer + docs, benchmark harnesses, examples that do not map to kitcn scaffolds, and + tests for behavior kitcn neither exposes nor depends on. +- Ambiguous optional: added test suites, e2e harnesses, examples, fixtures, + benchmark tooling, and dev-only utilities. Stop and ask before pulling these + in unless they are the direct verification path for a selected required fix. + +## 5. Choose One Implementation Slice + +Pick the highest-leverage slice using this order: + +1. security fix +2. compatibility breakage +3. bugfix that affects kitcn users +4. delete dirty hack made obsolete upstream +5. agentic or DX improvement for deterministic setup, CLI, or generated output +6. feature kitcn can expose cleanly +7. docs or skill-only update +8. optional tests or examples only after user approval + +If several relevant upstream fixes touch the same auth surface and do not +conflict, delegate them together. If they touch separate surfaces, pick the +highest-risk slice first. + +If the winning slice touches published package code, the delegated task must +update the active changeset and run `bun --cwd packages/kitcn build`. + +If it touches scaffold templates, the delegated task must run +`bun run fixtures:sync` and `bun run fixtures:check`. + +If it touches auth runtime, client, provider, or query invalidation surfaces, +the delegated task must follow the repo's auth verification lane. Do not import +a slow upstream e2e suite unless the user explicitly approves it. + +## 6. Delegate Through `task` + +Load +[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) with a +prompt in this exact shape: + +```md +Implement this convex-better-auth sync opportunity. + +Fork: zbeyens/convex-better-auth +Upstream: / +Range: .. +Behind: commits + +Opportunity: +Class: + +Evidence: +- Upstream commits: +- Upstream diff: +- Kitcn evidence: + +Implementation: +- +- +- + +Acceptance: +- +- +- +- open the PR after verification + +Do not preserve obsolete auth workarounds if the upstream change removes the +need for them. Hard cut the hack. +Do not add optional slow e2e suites, broad examples, or dev-only upstream test +infrastructure unless the user approved that scope. +``` + +Then follow `task` until the PR exists or a real blocker is proven. + +## Output + +Before delegation, keep the audit terse: + +```md +Fork: zbeyens/convex-better-auth +Upstream: / +Range: .. +Behind: + +| Class | Opportunity | Evidence | Decision | +| --- | --- | --- | --- | +| bugfix | ... | ... | selected | + +Delegating to task: +``` + +If the right choice is ambiguous, stop and ask one pointed question. Example: + +```md +Upstream added a Playwright e2e suite that does not fix a current kitcn bug. +Do you want that pulled in, or should I ignore it and keep this sync to runtime +fixes only? +``` + +After `task` finishes, use its final handoff format. diff --git a/.changeset/convex-135-agentic-bootstrap.md b/.changeset/convex-135-agentic-bootstrap.md index d993040f..96d8cc0d 100644 --- a/.changeset/convex-135-agentic-bootstrap.md +++ b/.changeset/convex-135-agentic-bootstrap.md @@ -8,3 +8,4 @@ - Let Convex handle anonymous non-interactive local setup without forcing `CONVEX_AGENT_MODE`. - Warn when an app pins an older Convex dependency family than kitcn expects. - Support Convex `dev --start` as a pre-run conflict flag. +- Add lazy Convex auth route registration and sync Convex Better Auth runtime fixes. diff --git a/.claude/skills/sync-convex-auth b/.claude/skills/sync-convex-auth new file mode 120000 index 00000000..8718db24 --- /dev/null +++ b/.claude/skills/sync-convex-auth @@ -0,0 +1 @@ +../../.agents/skills/sync-convex-auth \ No newline at end of file diff --git a/bun.lock b/bun.lock index 0bc80dfb..68c9d42f 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "kitcn", "dependencies": { - "@convex-dev/better-auth": "0.11.1", + "@convex-dev/better-auth": "0.11.4", "@tanstack/query-core": "5.90.20", "@tanstack/react-query": "5.95.2", "@tanstack/solid-query": "5.90.23", @@ -125,7 +125,7 @@ "dependencies": { "@babel/parser": "^7.28.4", "@clack/prompts": "^0.11.0", - "@convex-dev/better-auth": "^0.11.1", + "@convex-dev/better-auth": "^0.11.4", "chokidar": "^5.0.0", "common-tags": "^1.8.2", "diff": "^8.0.2", @@ -377,7 +377,7 @@ "@concavejs/runtime-bun": ["@concavejs/runtime-bun@0.0.1-alpha.14", "", { "dependencies": { "@concavejs/blobstore-bun-fs": "0.0.1-alpha.14", "@concavejs/blobstore-bun-s3": "0.0.1-alpha.14", "@concavejs/core": "0.0.1-alpha.14", "@concavejs/docstore-bun-sqlite": "0.0.1-alpha.14", "@concavejs/runtime-base": "0.0.1-alpha.14", "convex": "^1.33.1" } }, "sha512-YZihihRHcnyWgeiCcTCrbwzb/jXW79ozhpAV037FySggaKGrWZFfWWx6ekOZlelO+R+sbtpm21+bbgpkiRSpkQ=="], - "@convex-dev/better-auth": ["@convex-dev/better-auth@0.11.1", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": ">=1.5.0 <1.6.0", "convex": "^1.25.0", "react": "^18.3.1 || ^19.0.0" } }, "sha512-12EdVfNMeKB8fnGMsW4NEEkOQ0LW672qnFoA0WDfM3qGBrbySaVZ8B5OZZk+8ZRbeyUwXtVg8oFvtLejEwPRpA=="], + "@convex-dev/better-auth": ["@convex-dev/better-auth@0.11.4", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": ">=1.5.0 <1.6.0", "convex": "^1.25.0", "react": "^18.3.1 || ^19.0.0" } }, "sha512-CsRsM7UaQgQKH1mt4z64QOZUfrhZKNsspkpWjlhqLiIz5THfnM2vaSVMQ3GOddcUvw7AgkNCNR3u8C8HjLQEMA=="], "@convex-dev/react-query": ["@convex-dev/react-query@0.1.0", "", { "peerDependencies": { "@tanstack/react-query": "^5.0.0", "convex": "^1.29.3" } }, "sha512-ULmBZCtAQDUePdBhUv7hj1Yy4QiCelhi6uEIOtMpjW8db6W9CFKvQwLt9heLngccFyMFfAZdOfSrT+cP4AH0Jw=="], diff --git a/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md b/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md new file mode 100644 index 00000000..598124f5 --- /dev/null +++ b/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md @@ -0,0 +1,119 @@ +--- +title: Convex Better Auth upstream sync must filter runtime fixes from repo churn +date: 2026-04-16 +category: integration-issues +module: auth-upstream-sync +problem_type: integration_issue +component: authentication +symptoms: + - `zbeyens/convex-better-auth` can be behind upstream without GitHub reporting a fork parent + - upstream runtime fixes can be mixed with release config, npmrc, renovate, and test-suite churn + - kitcn auth routes can initialize Better Auth during `convex/http.ts` registration + - adapter queries can miss composite indexes when upstream fixes index field matching +root_cause: missing_workflow_step +resolution_type: code_fix +severity: medium +tags: [auth, convex-better-auth, upstream-sync, register-routes, adapter] +--- + +# Convex Better Auth upstream sync must filter runtime fixes from repo churn + +## Problem + +Syncing `@convex-dev/better-auth` cannot be a blind package bump. The upstream +range can include runtime fixes, docs, release setup, test harness rewrites, and +repo maintenance in the same commit window. + +For the `0.11.1` to `0.11.4` range, the relevant kitcn work was the auth +runtime slice: lazy route registration, composite index matching, narrower JWT +type imports, and the package dependency bump. + +## Symptoms + +- `gh repo view zbeyens/convex-better-auth` reported `parent: null`, even + though npm metadata and the local clone proved the upstream repo was + `get-convex/better-auth`. +- `git rev-list fork/main..origin/main` showed 17 upstream commits. +- the upstream diff mixed useful auth runtime changes with `.npmrc`, renovate, + release script, generated component type, and optional test-suite changes. +- kitcn already had some upstream fixes, such as numeric date outputs and + `BaseURLConfig` support, so applying the whole diff would duplicate work and + import noise. + +## What Didn't Work + +- Treating GitHub fork metadata as authoritative was not enough. The repository + is effectively a fork, but GitHub did not expose a parent. +- Treating every upstream file change as relevant was too blunt. The diff + included useful fixes and repo-only maintenance in the same range. +- Pulling upstream test harness churn wholesale would have increased scope + without proving a kitcn user-facing fix. + +## Solution + +Use npm metadata and local remotes to prove upstream when GitHub fork metadata +is missing: + +```bash +npm view @convex-dev/better-auth repository homepage version --json +gh repo view get-convex/better-auth --json nameWithOwner,defaultBranchRef,url +git -C ../convex-better-auth remote add fork https://github.com/zbeyens/convex-better-auth.git +git -C ../convex-better-auth fetch origin main --tags +git -C ../convex-better-auth fetch fork main --tags +git -C ../convex-better-auth rev-list --count fork/main..origin/main +``` + +Then classify upstream commits by kitcn impact: + +- pull runtime fixes that affect kitcn imports, exports, wrappers, auth routes, + generated contracts, and adapter behavior +- skip release plumbing, renovate config, npmrc files, and upstream-only test + policy rewrites unless they directly verify the selected fix +- compare each upstream fix with local code before applying it, because kitcn + may already carry equivalent fixes + +For the `0.11.4` sync, the selected kitcn slice was: + +- bump `@convex-dev/better-auth` to `0.11.4` +- add `registerRoutesLazy` to `kitcn/auth/http` +- keep `registerRoutes` eager for existing behavior, while making the lazy + helper default to `/api/auth` unless `basePath` is passed +- fix adapter composite index lookup to use real Convex field names, not + underscore-prefixed field names +- import `JwtOptions` from `better-auth/plugins/jwt` +- document the lazy route helper in `www` and the packaged Convex skill + +## Why This Works + +The upstream range had three distinct classes of change: + +1. already-applied fixes: numeric date output and `BaseURLConfig` typing +2. relevant runtime fixes: lazy route registration, subpath JWT typing, and + composite index matching +3. irrelevant or optional churn: renovate, npmrc, release scripts, generated + type freshness, and upstream test harness cleanup + +Filtering by kitcn's auth surfaces kept the sync small and useful. The lazy +route helper gives plain Convex auth users a way to avoid Better Auth +initialization during `convex/http.ts` registration. The adapter index fix +prevents full scans when Better Auth queries combine an equality predicate with +a `sortBy` field on a composite index. + +## Prevention + +1. When GitHub fork metadata is missing, prove upstream through npm repository + metadata before asking the user. +2. For `convex-better-auth` syncs, inspect local kitcn equivalents before + applying upstream patches. Some fixes may already exist locally. +3. Keep optional upstream test suites and repo maintenance out of the PR unless + they are the direct proof path for the selected runtime fix. +4. Add focused regression tests for the pulled behavior: + - lazy auth route registration must not call `getAuth({})` during + registration when `basePath` and `trustedOrigins` are supplied + - adapter pagination must select composite indexes using real field names + +## Related Issues + +- `docs/solutions/integration-issues/plain-codegen-must-not-import-managed-auth-convex-plugin-20260325.md` +- `docs/solutions/integration-issues/better-auth-1-5-generated-auth-runtime-typing.md` +- `docs/solutions/integration-issues/convex-auth-jwks-routes-should-not-trigger-better-auth-ip-warnings-20260325.md` diff --git a/package.json b/package.json index ac56e41c..e69660fc 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "typecheck:watch": "tsc --noEmit --watch" }, "dependencies": { - "@convex-dev/better-auth": "0.11.1", + "@convex-dev/better-auth": "0.11.4", "@tanstack/query-core": "5.90.20", "@tanstack/react-query": "5.95.2", "@tanstack/solid-query": "5.90.23", diff --git a/packages/kitcn/package.json b/packages/kitcn/package.json index f5f11d2f..b34d971a 100644 --- a/packages/kitcn/package.json +++ b/packages/kitcn/package.json @@ -59,7 +59,7 @@ "dependencies": { "@babel/parser": "^7.28.4", "@clack/prompts": "^0.11.0", - "@convex-dev/better-auth": "^0.11.1", + "@convex-dev/better-auth": "^0.11.4", "chokidar": "^5.0.0", "common-tags": "^1.8.2", "diff": "^8.0.2", diff --git a/packages/kitcn/skills/convex/references/features/auth.md b/packages/kitcn/skills/convex/references/features/auth.md index d8f07d33..ee4b70fa 100644 --- a/packages/kitcn/skills/convex/references/features/auth.md +++ b/packages/kitcn/skills/convex/references/features/auth.md @@ -161,6 +161,8 @@ If you want to own the auth tables by hand, use `setup/server.md`. Import auth route helpers from `kitcn/auth/http`. That entrypoint auto-installs the Convex-safe `MessageChannel` polyfill. +Use `registerRoutesLazy` for plain Convex auth routes so Better Auth does not +initialize during `convex/http.ts` registration. ### 7. HTTP Routes @@ -186,6 +188,25 @@ app.use(authMiddleware(getAuth)); export default createHttpRouter(app, httpRouter); ``` +```ts +// convex/functions/http.ts - plain Convex option +import { registerRoutesLazy } from 'kitcn/auth/http'; +import { httpRouter } from 'convex/server'; +import { getAuth } from './generated/auth'; + +const http = httpRouter(); + +registerRoutesLazy(http, getAuth, { + basePath: '/api/auth', + cors: { + allowedOrigins: [process.env.SITE_URL!], + }, + trustedOrigins: [process.env.SITE_URL!], +}); + +export default http; +``` + ### 8. Environment Variables ```bash diff --git a/packages/kitcn/skills/convex/references/setup/auth.md b/packages/kitcn/skills/convex/references/setup/auth.md index cbb83dd5..7cb246f3 100644 --- a/packages/kitcn/skills/convex/references/setup/auth.md +++ b/packages/kitcn/skills/convex/references/setup/auth.md @@ -198,8 +198,9 @@ Keep all auth reads/writes on ORM table definitions in `convex/functions/schema. ### 6.5 Register auth HTTP routes -Use `kitcn/auth/http` for `authMiddleware` or `registerRoutes`. +Use `kitcn/auth/http` for `authMiddleware`, `registerRoutes`, or `registerRoutesLazy`. It auto-installs the Convex-safe `MessageChannel` polyfill, so no manual `http-polyfills.ts` file is needed. +Use `registerRoutesLazy` for plain Convex auth routes when `convex/http.ts` should avoid initializing Better Auth during registration. **Create:** `convex/functions/http.ts` diff --git a/packages/kitcn/src/auth-http/index.ts b/packages/kitcn/src/auth-http/index.ts index 96a5e23d..8cda6351 100644 --- a/packages/kitcn/src/auth-http/index.ts +++ b/packages/kitcn/src/auth-http/index.ts @@ -41,4 +41,4 @@ export function installAuthHttpPolyfills(): void { installAuthHttpPolyfills(); export { authMiddleware } from '../auth/middleware'; -export { registerRoutes } from '../auth/registerRoutes'; +export { registerRoutes, registerRoutesLazy } from '../auth/registerRoutes'; diff --git a/packages/kitcn/src/auth/adapter-utils.test.ts b/packages/kitcn/src/auth/adapter-utils.test.ts index 84adf43c..36e9e310 100644 --- a/packages/kitcn/src/auth/adapter-utils.test.ts +++ b/packages/kitcn/src/auth/adapter-utils.test.ts @@ -1,6 +1,7 @@ import { checkUniqueFields, hasUniqueFields, + paginate, selectFields, } from './adapter-utils'; @@ -50,6 +51,90 @@ describe('selectFields', () => { }); }); +describe('paginate', () => { + test('uses composite indexes with real field names for eq plus sortBy', async () => { + const indexCalls: Array<{ indexName: string }> = []; + const rangeCalls: Array<{ field: string; value: unknown }> = []; + const queryBuilder = { + eq: (field: string, value: unknown) => { + rangeCalls.push({ field, value }); + return queryBuilder; + }, + }; + const db = { + query: () => ({ + withIndex: ( + indexName: string, + applyRange?: (q: typeof queryBuilder) => unknown + ) => { + indexCalls.push({ indexName }); + applyRange?.(queryBuilder); + + return { + order: () => ({ + async *[Symbol.asyncIterator]() { + yield { + _creationTime: 1, + _id: 'account-1', + accountId: 'acct_1', + providerId: 'github', + }; + }, + }), + }; + }, + }), + }; + const warnSpy = spyOn(console, 'warn').mockImplementation(() => {}); + + try { + const result = await paginate( + { db } as any, + { + tables: { + account: { + indexes: [ + { + fields: ['providerId', 'accountId'], + indexDescriptor: 'by_provider_account', + }, + ], + export: () => ({ + indexes: [ + { + fields: ['providerId', 'accountId'], + indexDescriptor: 'by_provider_account', + }, + ], + }), + }, + }, + } as any, + {} as any, + { + model: 'account', + paginationOpts: { cursor: null, numItems: 10 }, + sortBy: { direction: 'asc', field: 'accountId' }, + where: [ + { + field: 'providerId', + operator: 'eq', + value: 'github', + }, + ], + } + ); + + expect(indexCalls[0]?.indexName).toBe('by_provider_account'); + expect(rangeCalls).toEqual([{ field: 'providerId', value: 'github' }]); + expect(warnSpy).not.toHaveBeenCalled(); + expect(result.page).toHaveLength(1); + } finally { + warnSpy.mockRestore(); + } + }); +}); + describe('checkUniqueFields', () => { test('returns early when no unique fields are present in input', async () => { const querySpy = spyOn( diff --git a/packages/kitcn/src/auth/adapter-utils.ts b/packages/kitcn/src/auth/adapter-utils.ts index 5a7f611f..2171a541 100644 --- a/packages/kitcn/src/auth/adapter-utils.ts +++ b/packages/kitcn/src/auth/adapter-utils.ts @@ -218,14 +218,10 @@ const findIndex = ( // We internally use _creationTime in place of Better Auth's createdAt const indexFields = indexEqFields .map(([field]) => field) - .concat( - boundField && boundField !== 'createdAt' - ? `${indexEqFields.length > 0 ? '_' : ''}${boundField}` - : '' - ) + .concat(boundField && boundField !== 'createdAt' ? boundField : '') .concat( sortField && sortField !== 'createdAt' && boundField !== sortField - ? `${indexEqFields.length > 0 || boundField ? '_' : ''}${sortField}` + ? sortField : '' ) .filter(Boolean); diff --git a/packages/kitcn/src/auth/auth-config.ts b/packages/kitcn/src/auth/auth-config.ts index 04e154a3..f0f90134 100644 --- a/packages/kitcn/src/auth/auth-config.ts +++ b/packages/kitcn/src/auth/auth-config.ts @@ -1,4 +1,4 @@ -import type { JwtOptions } from 'better-auth/plugins'; +import type { JwtOptions } from 'better-auth/plugins/jwt'; import type { AuthProvider } from 'convex/server'; type JwksDoc = { diff --git a/packages/kitcn/src/auth/registerRoutes.test.ts b/packages/kitcn/src/auth/registerRoutes.test.ts index d1e8d7e5..16428a39 100644 --- a/packages/kitcn/src/auth/registerRoutes.test.ts +++ b/packages/kitcn/src/auth/registerRoutes.test.ts @@ -1,7 +1,7 @@ import { httpRouter } from 'convex/server'; import { Request as UndiciRequest } from 'undici'; -import { registerRoutes } from './registerRoutes'; +import { registerRoutes, registerRoutesLazy } from './registerRoutes'; const unwrapLocation = (response: Response) => response.headers.get('location') ?? response.headers.get('Location'); @@ -32,6 +32,67 @@ const unwrapInvoke = async ( }; describe('registerRoutes', () => { + test('lazy registration avoids constructing auth until a request arrives', async () => { + const http = httpRouter(); + const authHandler = mock(async () => new Response('ok')); + const getAuth = mock(() => ({ + handler: authHandler, + options: { basePath: '/custom-auth' }, + $context: Promise.resolve({ options: { trustedOrigins: [] } }), + })); + + registerRoutesLazy(http as any, getAuth as any, { + basePath: '/api/auth', + cors: false, + }); + + expect(getAuth).not.toHaveBeenCalled(); + expect(http.lookup('/api/auth/session', 'GET')).not.toBe(null); + + const authGet = http.lookup('/api/auth/session', 'GET')!; + const authRes = await unwrapInvoke( + authGet[0], + new UndiciRequest('https://example.convex.site/api/auth/session', { + method: 'GET', + }) as any + ); + + expect(await authRes.text()).toBe('ok'); + expect(getAuth).toHaveBeenCalledTimes(1); + expect(authHandler).toHaveBeenCalledTimes(1); + }); + + test('lazy CORS registration can use explicit trusted origins without constructing auth', async () => { + const http = httpRouter(); + const getAuth = mock(() => ({ + handler: async () => new Response('ok'), + options: { basePath: '/api/auth' }, + $context: Promise.resolve({ options: { trustedOrigins: [] } }), + })); + + registerRoutesLazy(http as any, getAuth as any, { + cors: true, + trustedOrigins: ['https://trusted.example'], + }); + + expect(getAuth).not.toHaveBeenCalled(); + + const optionsMatch = http.lookup('/api/auth/session', 'OPTIONS')!; + const optionsRes = await unwrapInvoke( + optionsMatch[0], + new UndiciRequest('https://example.convex.site/api/auth/session', { + headers: { origin: 'https://trusted.example' }, + method: 'OPTIONS', + }) as any + ); + + expect(optionsRes.status).toBe(204); + expect(optionsRes.headers.get('access-control-allow-origin')).toBe( + 'https://trusted.example' + ); + expect(getAuth).not.toHaveBeenCalled(); + }); + test('registers well-known redirect and GET/POST auth routes when cors is disabled', async () => { const previous = process.env.CONVEX_SITE_URL; process.env.CONVEX_SITE_URL = 'https://example.convex.site'; diff --git a/packages/kitcn/src/auth/registerRoutes.ts b/packages/kitcn/src/auth/registerRoutes.ts index be0c5dcf..2a5c6908 100644 --- a/packages/kitcn/src/auth/registerRoutes.ts +++ b/packages/kitcn/src/auth/registerRoutes.ts @@ -8,6 +8,25 @@ import type { GetAuth } from './types'; type TrustedOriginsOption = BetterAuthOptions['trustedOrigins']; +type RouteCorsOptions = + | { + // These values are appended to the default values + allowedHeaders?: string[]; + allowedOrigins?: string[]; + exposedHeaders?: string[]; + } + | boolean; + +type RegisterRoutesOptions = { + cors?: RouteCorsOptions; + verbose?: boolean; +}; + +type RegisterRoutesLazyOptions = RegisterRoutesOptions & { + basePath?: string; + trustedOrigins?: TrustedOriginsOption; +}; + type AuthRouteContract = { $context: Promise<{ options: { @@ -22,6 +41,13 @@ type AuthRouteContract = { }; }; +type RouteRegistration = { + getAuth: GetAuth; + getRegistrationAuth: () => AuthRouteContract; + path: string; + trustedOrigins?: TrustedOriginsOption; +}; + const LOCAL_AUTH_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']); const LOCAL_CONVEX_AUTH_IP_PATHS = new Set([ '/convex/.well-known/openid-configuration', @@ -72,26 +98,15 @@ const withLocalConvexAuthIp = (request: Request, basePath: string) => { }); }; -export const registerRoutes = ( +const registerAuthRoutes = ( http: HttpRouter, - getAuth: GetAuth, - opts: { - cors?: - | { - // These values are appended to the default values - allowedHeaders?: string[]; - allowedOrigins?: string[]; - exposedHeaders?: string[]; - } - | boolean; - verbose?: boolean; - } = {} + registration: RouteRegistration, + opts: RegisterRoutesOptions = {} ) => { - const staticAuth = getAuth({} as any); - const path = staticAuth.options.basePath ?? '/api/auth'; + const { getAuth, getRegistrationAuth, path } = registration; const authRequestHandler = httpActionGeneric(async (ctx, request) => { if (opts?.verbose) { - console.log('options.baseURL', staticAuth.options.baseURL); + console.log('options.baseURL', getRegistrationAuth().options.baseURL); console.log('request headers', request.headers); } @@ -166,7 +181,8 @@ export const registerRoutes = ( allowedOrigins: async (request) => { const resolvedTrustedOrigins = trustedOriginsOption ?? - (await staticAuth.$context).options.trustedOrigins ?? + registration.trustedOrigins ?? + (await getRegistrationAuth().$context).options.trustedOrigins ?? []; trustedOriginsOption = resolvedTrustedOrigins; const rawOrigins = Array.isArray(resolvedTrustedOrigins) @@ -199,3 +215,45 @@ export const registerRoutes = ( pathPrefix: `${path}/`, }); }; + +export const registerRoutes = ( + http: HttpRouter, + getAuth: GetAuth, + opts: RegisterRoutesOptions = {} +) => { + const registrationAuth = getAuth({} as any); + + return registerAuthRoutes( + http, + { + getAuth, + getRegistrationAuth: () => registrationAuth, + path: registrationAuth.options.basePath ?? '/api/auth', + }, + opts + ); +}; + +export const registerRoutesLazy = ( + http: HttpRouter, + getAuth: GetAuth, + opts: RegisterRoutesLazyOptions = {} +) => { + let registrationAuth: AuthRouteContract | undefined; + const getRegistrationAuth = () => { + registrationAuth ??= getAuth({} as any); + + return registrationAuth; + }; + + return registerAuthRoutes( + http, + { + getAuth, + getRegistrationAuth, + path: opts.basePath ?? '/api/auth', + trustedOrigins: opts.trustedOrigins, + }, + opts + ); +}; diff --git a/packages/kitcn/src/auth/registerRoutes.types.ts b/packages/kitcn/src/auth/registerRoutes.types.ts index 68159eae..43fd5faa 100644 --- a/packages/kitcn/src/auth/registerRoutes.types.ts +++ b/packages/kitcn/src/auth/registerRoutes.types.ts @@ -1,9 +1,14 @@ import type { Auth, BetterAuthOptions } from 'better-auth'; import type { HttpRouter } from 'convex/server'; -import { registerRoutes } from './registerRoutes'; +import { registerRoutes, registerRoutesLazy } from './registerRoutes'; declare const http: HttpRouter; declare const getAuth: (ctx: unknown) => Auth; registerRoutes(http, getAuth, { cors: false }); +registerRoutesLazy(http, getAuth, { + basePath: '/api/auth', + cors: false, + trustedOrigins: ['https://example.com'], +}); diff --git a/www/content/docs/auth/server.mdx b/www/content/docs/auth/server.mdx index 7c3a8cb1..5ded11b7 100644 --- a/www/content/docs/auth/server.mdx +++ b/www/content/docs/auth/server.mdx @@ -169,16 +169,18 @@ tables change, rerun the same preset command. It does not support `--schema`. ```ts title="convex/functions/http.ts" showLineNumbers {1-4,6-13} - import { registerRoutes } from 'kitcn/auth/http'; + import { registerRoutesLazy } from 'kitcn/auth/http'; import { httpRouter } from 'convex/server'; import { getAuth } from './generated/auth'; const http = httpRouter(); - registerRoutes(http, getAuth, { + registerRoutesLazy(http, getAuth, { + basePath: '/api/auth', cors: { allowedOrigins: [process.env.SITE_URL!], }, + trustedOrigins: [process.env.SITE_URL!], }); export default http; From 909b10b4264cc46e7e5edaf529a6b65804cfc7ff Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 15:21:42 +0200 Subject: [PATCH 02/14] feat: make auth route registration lazy by default --- .changeset/convex-135-agentic-bootstrap.md | 31 +++++++++++++- ...th-upstream-sync-runtime-fixes-20260416.md | 11 +++-- .../skills/convex/references/features/auth.md | 11 +++-- .../skills/convex/references/setup/auth.md | 5 ++- packages/kitcn/src/auth-http/index.ts | 2 +- .../kitcn/src/auth/registerRoutes.test.ts | 24 ++++++----- packages/kitcn/src/auth/registerRoutes.ts | 40 ++++--------------- .../kitcn/src/auth/registerRoutes.types.ts | 5 +-- www/content/docs/auth/server.mdx | 6 +-- 9 files changed, 68 insertions(+), 67 deletions(-) diff --git a/.changeset/convex-135-agentic-bootstrap.md b/.changeset/convex-135-agentic-bootstrap.md index 96d8cc0d..53b39a15 100644 --- a/.changeset/convex-135-agentic-bootstrap.md +++ b/.changeset/convex-135-agentic-bootstrap.md @@ -1,11 +1,38 @@ --- -"kitcn": patch +"kitcn": minor "@kitcn/resend": patch --- +## Breaking changes + +- Remove `registerRoutesLazy` and make `registerRoutes` lazy by default. If you use a custom auth base path, pass `basePath` explicitly. + +```ts +// Before +import { registerRoutesLazy } from "kitcn/auth/http"; + +registerRoutesLazy(http, getAuth, { + basePath: "/custom-auth", + cors: { + allowedOrigins: [process.env.SITE_URL!], + }, + trustedOrigins: [process.env.SITE_URL!], +}); + +// After +import { registerRoutes } from "kitcn/auth/http"; + +registerRoutes(http, getAuth, { + basePath: "/custom-auth", + cors: { + allowedOrigins: [process.env.SITE_URL!], + }, +}); +``` + ## Patches - Let Convex handle anonymous non-interactive local setup without forcing `CONVEX_AGENT_MODE`. - Warn when an app pins an older Convex dependency family than kitcn expects. - Support Convex `dev --start` as a pre-run conflict flag. -- Add lazy Convex auth route registration and sync Convex Better Auth runtime fixes. +- Sync Convex Better Auth runtime fixes, including `@convex-dev/better-auth@0.11.4` and auth adapter index matching. diff --git a/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md b/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md index 598124f5..590eb38d 100644 --- a/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md +++ b/docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md @@ -75,13 +75,12 @@ Then classify upstream commits by kitcn impact: For the `0.11.4` sync, the selected kitcn slice was: - bump `@convex-dev/better-auth` to `0.11.4` -- add `registerRoutesLazy` to `kitcn/auth/http` -- keep `registerRoutes` eager for existing behavior, while making the lazy - helper default to `/api/auth` unless `basePath` is passed +- make `registerRoutes` lazy by default instead of adding a second public helper +- require explicit `basePath` only when the auth config uses a non-default path - fix adapter composite index lookup to use real Convex field names, not underscore-prefixed field names - import `JwtOptions` from `better-auth/plugins/jwt` -- document the lazy route helper in `www` and the packaged Convex skill +- document the lazy `registerRoutes` behavior in `www` and the packaged Convex skill ## Why This Works @@ -94,8 +93,8 @@ The upstream range had three distinct classes of change: type freshness, and upstream test harness cleanup Filtering by kitcn's auth surfaces kept the sync small and useful. The lazy -route helper gives plain Convex auth users a way to avoid Better Auth -initialization during `convex/http.ts` registration. The adapter index fix +`registerRoutes` helper now avoids Better Auth initialization during +`convex/http.ts` registration. The adapter index fix prevents full scans when Better Auth queries combine an equality predicate with a `sortBy` field on a composite index. diff --git a/packages/kitcn/skills/convex/references/features/auth.md b/packages/kitcn/skills/convex/references/features/auth.md index ee4b70fa..a8b0991d 100644 --- a/packages/kitcn/skills/convex/references/features/auth.md +++ b/packages/kitcn/skills/convex/references/features/auth.md @@ -161,8 +161,9 @@ If you want to own the auth tables by hand, use `setup/server.md`. Import auth route helpers from `kitcn/auth/http`. That entrypoint auto-installs the Convex-safe `MessageChannel` polyfill. -Use `registerRoutesLazy` for plain Convex auth routes so Better Auth does not -initialize during `convex/http.ts` registration. +`registerRoutes` is lazy by default, so Better Auth does not initialize during +`convex/http.ts` registration. If your auth config uses a custom base path, pass +the same `basePath` to `registerRoutes`. ### 7. HTTP Routes @@ -190,18 +191,16 @@ export default createHttpRouter(app, httpRouter); ```ts // convex/functions/http.ts - plain Convex option -import { registerRoutesLazy } from 'kitcn/auth/http'; +import { registerRoutes } from 'kitcn/auth/http'; import { httpRouter } from 'convex/server'; import { getAuth } from './generated/auth'; const http = httpRouter(); -registerRoutesLazy(http, getAuth, { - basePath: '/api/auth', +registerRoutes(http, getAuth, { cors: { allowedOrigins: [process.env.SITE_URL!], }, - trustedOrigins: [process.env.SITE_URL!], }); export default http; diff --git a/packages/kitcn/skills/convex/references/setup/auth.md b/packages/kitcn/skills/convex/references/setup/auth.md index 7cb246f3..a5a68f41 100644 --- a/packages/kitcn/skills/convex/references/setup/auth.md +++ b/packages/kitcn/skills/convex/references/setup/auth.md @@ -198,9 +198,10 @@ Keep all auth reads/writes on ORM table definitions in `convex/functions/schema. ### 6.5 Register auth HTTP routes -Use `kitcn/auth/http` for `authMiddleware`, `registerRoutes`, or `registerRoutesLazy`. +Use `kitcn/auth/http` for `authMiddleware` or `registerRoutes`. It auto-installs the Convex-safe `MessageChannel` polyfill, so no manual `http-polyfills.ts` file is needed. -Use `registerRoutesLazy` for plain Convex auth routes when `convex/http.ts` should avoid initializing Better Auth during registration. +`registerRoutes` is lazy by default. If the auth config uses a custom base path, +pass that same `basePath` in the route options. **Create:** `convex/functions/http.ts` diff --git a/packages/kitcn/src/auth-http/index.ts b/packages/kitcn/src/auth-http/index.ts index 8cda6351..96a5e23d 100644 --- a/packages/kitcn/src/auth-http/index.ts +++ b/packages/kitcn/src/auth-http/index.ts @@ -41,4 +41,4 @@ export function installAuthHttpPolyfills(): void { installAuthHttpPolyfills(); export { authMiddleware } from '../auth/middleware'; -export { registerRoutes, registerRoutesLazy } from '../auth/registerRoutes'; +export { registerRoutes } from '../auth/registerRoutes'; diff --git a/packages/kitcn/src/auth/registerRoutes.test.ts b/packages/kitcn/src/auth/registerRoutes.test.ts index 16428a39..75321484 100644 --- a/packages/kitcn/src/auth/registerRoutes.test.ts +++ b/packages/kitcn/src/auth/registerRoutes.test.ts @@ -1,7 +1,7 @@ import { httpRouter } from 'convex/server'; import { Request as UndiciRequest } from 'undici'; -import { registerRoutes, registerRoutesLazy } from './registerRoutes'; +import { registerRoutes } from './registerRoutes'; const unwrapLocation = (response: Response) => response.headers.get('location') ?? response.headers.get('Location'); @@ -32,7 +32,7 @@ const unwrapInvoke = async ( }; describe('registerRoutes', () => { - test('lazy registration avoids constructing auth until a request arrives', async () => { + test('does not construct auth until a request arrives', async () => { const http = httpRouter(); const authHandler = mock(async () => new Response('ok')); const getAuth = mock(() => ({ @@ -41,7 +41,7 @@ describe('registerRoutes', () => { $context: Promise.resolve({ options: { trustedOrigins: [] } }), })); - registerRoutesLazy(http as any, getAuth as any, { + registerRoutes(http as any, getAuth as any, { basePath: '/api/auth', cors: false, }); @@ -62,18 +62,17 @@ describe('registerRoutes', () => { expect(authHandler).toHaveBeenCalledTimes(1); }); - test('lazy CORS registration can use explicit trusted origins without constructing auth', async () => { + test('resolves trusted origins lazily from auth context for CORS', async () => { const http = httpRouter(); const getAuth = mock(() => ({ handler: async () => new Response('ok'), options: { basePath: '/api/auth' }, - $context: Promise.resolve({ options: { trustedOrigins: [] } }), + $context: Promise.resolve({ + options: { trustedOrigins: ['https://trusted.example*'] }, + }), })); - registerRoutesLazy(http as any, getAuth as any, { - cors: true, - trustedOrigins: ['https://trusted.example'], - }); + registerRoutes(http as any, getAuth as any, { cors: true }); expect(getAuth).not.toHaveBeenCalled(); @@ -90,7 +89,7 @@ describe('registerRoutes', () => { expect(optionsRes.headers.get('access-control-allow-origin')).toBe( 'https://trusted.example' ); - expect(getAuth).not.toHaveBeenCalled(); + expect(getAuth).toHaveBeenCalledTimes(1); }); test('registers well-known redirect and GET/POST auth routes when cors is disabled', async () => { @@ -193,7 +192,10 @@ describe('registerRoutes', () => { $context: Promise.resolve({ options: { trustedOrigins: [] } }), }); - registerRoutes(http as any, getAuth as any, { cors: false }); + registerRoutes(http as any, getAuth as any, { + basePath: '/auth', + cors: false, + }); const lookedUp = http.lookup('/.well-known/openid-configuration', 'GET')!; expect(lookedUp[0]).toBe(wellKnownHandler as any); diff --git a/packages/kitcn/src/auth/registerRoutes.ts b/packages/kitcn/src/auth/registerRoutes.ts index 2a5c6908..74fbda44 100644 --- a/packages/kitcn/src/auth/registerRoutes.ts +++ b/packages/kitcn/src/auth/registerRoutes.ts @@ -18,15 +18,11 @@ type RouteCorsOptions = | boolean; type RegisterRoutesOptions = { + basePath?: string; cors?: RouteCorsOptions; verbose?: boolean; }; -type RegisterRoutesLazyOptions = RegisterRoutesOptions & { - basePath?: string; - trustedOrigins?: TrustedOriginsOption; -}; - type AuthRouteContract = { $context: Promise<{ options: { @@ -45,7 +41,6 @@ type RouteRegistration = { getAuth: GetAuth; getRegistrationAuth: () => AuthRouteContract; path: string; - trustedOrigins?: TrustedOriginsOption; }; const LOCAL_AUTH_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']); @@ -181,7 +176,6 @@ const registerAuthRoutes = ( allowedOrigins: async (request) => { const resolvedTrustedOrigins = trustedOriginsOption ?? - registration.trustedOrigins ?? (await getRegistrationAuth().$context).options.trustedOrigins ?? []; trustedOriginsOption = resolvedTrustedOrigins; @@ -221,38 +215,20 @@ export const registerRoutes = ( getAuth: GetAuth, opts: RegisterRoutesOptions = {} ) => { - const registrationAuth = getAuth({} as any); - return registerAuthRoutes( http, { getAuth, - getRegistrationAuth: () => registrationAuth, - path: registrationAuth.options.basePath ?? '/api/auth', - }, - opts - ); -}; + getRegistrationAuth: (() => { + let registrationAuth: AuthRouteContract | undefined; -export const registerRoutesLazy = ( - http: HttpRouter, - getAuth: GetAuth, - opts: RegisterRoutesLazyOptions = {} -) => { - let registrationAuth: AuthRouteContract | undefined; - const getRegistrationAuth = () => { - registrationAuth ??= getAuth({} as any); - - return registrationAuth; - }; + return () => { + registrationAuth ??= getAuth({} as any); - return registerAuthRoutes( - http, - { - getAuth, - getRegistrationAuth, + return registrationAuth; + }; + })(), path: opts.basePath ?? '/api/auth', - trustedOrigins: opts.trustedOrigins, }, opts ); diff --git a/packages/kitcn/src/auth/registerRoutes.types.ts b/packages/kitcn/src/auth/registerRoutes.types.ts index 43fd5faa..20dfa0a0 100644 --- a/packages/kitcn/src/auth/registerRoutes.types.ts +++ b/packages/kitcn/src/auth/registerRoutes.types.ts @@ -1,14 +1,13 @@ import type { Auth, BetterAuthOptions } from 'better-auth'; import type { HttpRouter } from 'convex/server'; -import { registerRoutes, registerRoutesLazy } from './registerRoutes'; +import { registerRoutes } from './registerRoutes'; declare const http: HttpRouter; declare const getAuth: (ctx: unknown) => Auth; registerRoutes(http, getAuth, { cors: false }); -registerRoutesLazy(http, getAuth, { +registerRoutes(http, getAuth, { basePath: '/api/auth', cors: false, - trustedOrigins: ['https://example.com'], }); diff --git a/www/content/docs/auth/server.mdx b/www/content/docs/auth/server.mdx index 5ded11b7..7c3a8cb1 100644 --- a/www/content/docs/auth/server.mdx +++ b/www/content/docs/auth/server.mdx @@ -169,18 +169,16 @@ tables change, rerun the same preset command. It does not support `--schema`. ```ts title="convex/functions/http.ts" showLineNumbers {1-4,6-13} - import { registerRoutesLazy } from 'kitcn/auth/http'; + import { registerRoutes } from 'kitcn/auth/http'; import { httpRouter } from 'convex/server'; import { getAuth } from './generated/auth'; const http = httpRouter(); - registerRoutesLazy(http, getAuth, { - basePath: '/api/auth', + registerRoutes(http, getAuth, { cors: { allowedOrigins: [process.env.SITE_URL!], }, - trustedOrigins: [process.env.SITE_URL!], }); export default http; From 544f0a9e728cc4466a25b9d9afa4564c6a71a0ce Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 15:29:20 +0200 Subject: [PATCH 03/14] Fix changeset to match auth route breaking change --- .changeset/convex-135-agentic-bootstrap.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/convex-135-agentic-bootstrap.md b/.changeset/convex-135-agentic-bootstrap.md index 53b39a15..22f72d79 100644 --- a/.changeset/convex-135-agentic-bootstrap.md +++ b/.changeset/convex-135-agentic-bootstrap.md @@ -1,22 +1,20 @@ --- "kitcn": minor -"@kitcn/resend": patch --- ## Breaking changes -- Remove `registerRoutesLazy` and make `registerRoutes` lazy by default. If you use a custom auth base path, pass `basePath` explicitly. +- Require explicit `basePath` when `registerRoutes` is used with non-default auth routes. ```ts // Before -import { registerRoutesLazy } from "kitcn/auth/http"; +import { registerRoutes } from "kitcn/auth/http"; -registerRoutesLazy(http, getAuth, { - basePath: "/custom-auth", +// auth config uses basePath: "/custom-auth" +registerRoutes(http, getAuth, { cors: { allowedOrigins: [process.env.SITE_URL!], }, - trustedOrigins: [process.env.SITE_URL!], }); // After @@ -35,4 +33,6 @@ registerRoutes(http, getAuth, { - Let Convex handle anonymous non-interactive local setup without forcing `CONVEX_AGENT_MODE`. - Warn when an app pins an older Convex dependency family than kitcn expects. - Support Convex `dev --start` as a pre-run conflict flag. -- Sync Convex Better Auth runtime fixes, including `@convex-dev/better-auth@0.11.4` and auth adapter index matching. +- Improve auth route registration so default Convex auth routes avoid eager Better Auth initialization during startup. +- Fix Better Auth adapter index matching for composite equality-plus-sort queries. +- Support `@convex-dev/better-auth@0.11.4`. From 442c7e548dabb7a86bb8aacf10494ec82bdd176a Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 17:14:45 +0200 Subject: [PATCH 04/14] feat: support better-auth 1.6.5 --- .changeset/convex-135-agentic-bootstrap.md | 11 ++ ...tructural-convex-auth-wrappers-20260416.md | 87 +++++++++++++ example/package.json | 2 +- example/src/lib/convex/auth-client.ts | 35 +++++- fixtures/next-auth/lib/convex/auth-client.ts | 23 +++- fixtures/next-auth/package.json | 2 +- fixtures/start-auth/package.json | 2 +- .../start-auth/src/lib/convex/auth-client.ts | 23 +++- fixtures/vite-auth/package.json | 2 +- .../vite-auth/src/lib/convex/auth-client.ts | 23 +++- package.json | 2 +- packages/kitcn/package.json | 2 +- .../src/auth-client/convex-auth-provider.tsx | 50 +++++++- packages/kitcn/src/auth-client/index.ts | 11 +- packages/kitcn/src/auth/adapter-utils.ts | 54 ++++++-- packages/kitcn/src/auth/index.ts | 10 +- .../items/auth/auth-client.template.ts | 97 ++++++++++++++- .../src/cli/supported-dependencies.test.ts | 6 +- .../kitcn/src/cli/supported-dependencies.ts | 2 +- packages/kitcn/src/react/auth-mutations.ts | 115 ++++++++++-------- packages/kitcn/src/solid/types.ts | 8 +- www/content/docs/nextjs/index.mdx | 2 +- 22 files changed, 477 insertions(+), 92 deletions(-) create mode 100644 docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md diff --git a/.changeset/convex-135-agentic-bootstrap.md b/.changeset/convex-135-agentic-bootstrap.md index 22f72d79..d3097264 100644 --- a/.changeset/convex-135-agentic-bootstrap.md +++ b/.changeset/convex-135-agentic-bootstrap.md @@ -28,6 +28,16 @@ registerRoutes(http, getAuth, { }); ``` +- Require `better-auth@1.6.5`. + +```bash +# Before +bun add better-auth@1.5.3 + +# After +bun add better-auth@1.6.5 +``` + ## Patches - Let Convex handle anonymous non-interactive local setup without forcing `CONVEX_AGENT_MODE`. @@ -36,3 +46,4 @@ registerRoutes(http, getAuth, { - Improve auth route registration so default Convex auth routes avoid eager Better Auth initialization during startup. - Fix Better Auth adapter index matching for composite equality-plus-sort queries. - Support `@convex-dev/better-auth@0.11.4`. +- Support the Better Auth 1.6 client surface in generated auth clients and providers. diff --git a/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md b/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md new file mode 100644 index 00000000..bbe5e7a7 --- /dev/null +++ b/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md @@ -0,0 +1,87 @@ +--- +title: Better Auth 1.6 support needs structural Convex auth wrappers +date: 2026-04-16 +category: integration-issues +module: auth-client +problem_type: integration_issue +component: authentication +symptoms: + - upgrading `better-auth` from `1.5.3` to `1.6.5` breaks typecheck in kitcn auth clients and generated auth apps + - `createAuthMutations(authClient)` reports missing `signIn`, `signOut`, or `signUp` even though those methods exist at runtime + - generated auth pages see `authClient.useSession().data` collapse to `never` +root_cause: wrong_api +resolution_type: code_fix +severity: high +tags: [better-auth, auth, convex, wrappers, typecheck] +--- + +# Better Auth 1.6 support needs structural Convex auth wrappers + +## Problem + +`kitcn` uses `@convex-dev/better-auth` for the Convex plugin and client plugin, +but Better Auth `1.6.x` changed enough of the client and plugin type surface +that the old exported types no longer compose cleanly. + +Runtime behavior was mostly fine. TypeScript was the part that exploded. + +## Symptoms + +- `createAuthMutations(authClient)` rejects the auth client because TypeScript + thinks core auth methods are missing +- `ConvexAuthProvider` rejects the auth client prop for the same reason +- generated auth pages cannot read `authClient.useSession().data.user` +- the richer example auth client loses organization and anonymous helper types + +## What Didn't Work + +- bumping version pins alone +- trusting direct re-exports from `@convex-dev/better-auth` +- expecting Better Auth `1.6.x` to infer the same client shape through the old + Convex plugin types + +## Solution + +Treat the Convex plugin and client plugin as structurally compatible with the +current Better Auth interfaces instead of inheriting the old package's stricter +generic types. + +Key changes: + +- wrap `convex` and `convexClient` in `kitcn` and cast them to the current + `BetterAuthPlugin` / `BetterAuthClientPlugin` shape +- make `createAuthMutations()` accept a structural auth client contract instead + of requiring perfect generic inference +- do the same for `ConvexAuthProvider` +- add a local `mode` field to the auth adapter `where` validator so Better Auth + `1.6` queries do not fail validation +- cast generated auth client templates and the example auth client through + `unknown` to a stable local interface with the methods the app actually uses + +## Why This Works + +The runtime object already had the right methods. The breakage was in the type +bridge between: + +1. Better Auth `1.6.x` +2. `@convex-dev/better-auth@0.11.4` +3. kitcn's wrappers and generated auth client files + +By making kitcn depend on structural contracts at the boundaries, we stop +TypeScript from forcing those three packages to agree on every internal generic +detail before the app can compile. + +## Prevention + +1. When a dependency wrapper sits between two fast-moving auth libraries, + prefer structural boundary types over direct re-exports of deep generic + contracts. +2. If a version bump fails only in generated apps, fix the template type shape, + not just the hand-written example app. +3. Re-run `bun typecheck`, `bun run fixtures:check`, and `bun check` after any + auth client type widening. The generated apps are the real proof. + +## Related Issues + +- `docs/solutions/integration-issues/better-auth-1-5-generated-auth-runtime-typing.md` +- `docs/solutions/integration-issues/convex-better-auth-upstream-sync-runtime-fixes-20260416.md` diff --git a/example/package.json b/example/package.json index b194bfee..05255182 100644 --- a/example/package.json +++ b/example/package.json @@ -37,7 +37,7 @@ "@react-email/components": "1.0.8", "@react-email/render": "2.0.4", "@t3-oss/env-nextjs": "0.13.10", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "kitcn": "workspace:*", "class-variance-authority": "0.7.1", "clsx": "2.1.1", diff --git a/example/src/lib/convex/auth-client.ts b/example/src/lib/convex/auth-client.ts index dfb9ba32..8906b00a 100644 --- a/example/src/lib/convex/auth-client.ts +++ b/example/src/lib/convex/auth-client.ts @@ -10,6 +10,39 @@ import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; import { env } from '@/env'; +type ExampleAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + anonymous: (args?: unknown) => Promise; + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useActiveOrganization: () => unknown; + useListOrganizations: () => unknown; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; + organization: { + checkRolePermission: (args: { + permissions: unknown; + role?: string | null; + }) => unknown; + listMembers: (args: unknown) => Promise<{ + error?: { message?: string }; + }>; + }; +}; + export const authClient = createAuthClient({ baseURL: env.NEXT_PUBLIC_SITE_URL, sessionOptions: { @@ -26,7 +59,7 @@ export const authClient = createAuthClient({ }), convexClient(), ], -}); +}) as unknown as ExampleAuthClient; // Export hooks from the auth client export const { useActiveOrganization, useListOrganizations } = authClient; diff --git a/fixtures/next-auth/lib/convex/auth-client.ts b/fixtures/next-auth/lib/convex/auth-client.ts index 48cfb855..ccefcd7c 100644 --- a/fixtures/next-auth/lib/convex/auth-client.ts +++ b/fixtures/next-auth/lib/convex/auth-client.ts @@ -2,10 +2,31 @@ import { createAuthClient } from 'better-auth/react'; import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; +type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; export const { useSignInMutationOptions, diff --git a/fixtures/next-auth/package.json b/fixtures/next-auth/package.json index 2945c3eb..1d77dec5 100644 --- a/fixtures/next-auth/package.json +++ b/fixtures/next-auth/package.json @@ -3,7 +3,7 @@ "@base-ui/react": "^1.4.0", "@opentelemetry/api": "1.9.0", "@tanstack/react-query": "5.95.2", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "1.35.1", diff --git a/fixtures/start-auth/package.json b/fixtures/start-auth/package.json index 2d9705c0..47542089 100644 --- a/fixtures/start-auth/package.json +++ b/fixtures/start-auth/package.json @@ -11,7 +11,7 @@ "@tanstack/react-router-ssr-query": "^1.131.7", "@tanstack/react-start": "^1.132.0", "@tanstack/router-plugin": "^1.132.0", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "1.35.1", diff --git a/fixtures/start-auth/src/lib/convex/auth-client.ts b/fixtures/start-auth/src/lib/convex/auth-client.ts index cc2cb480..612b6762 100644 --- a/fixtures/start-auth/src/lib/convex/auth-client.ts +++ b/fixtures/start-auth/src/lib/convex/auth-client.ts @@ -2,13 +2,34 @@ import { createAuthClient } from 'better-auth/react'; import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; +type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: typeof window === 'undefined' ? (import.meta.env.VITE_SITE_URL as string | undefined) : window.location.origin, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; export const { useSignInMutationOptions, diff --git a/fixtures/vite-auth/package.json b/fixtures/vite-auth/package.json index a66c2e5f..4249df8e 100644 --- a/fixtures/vite-auth/package.json +++ b/fixtures/vite-auth/package.json @@ -5,7 +5,7 @@ "@opentelemetry/api": "1.9.0", "@tailwindcss/vite": "^4.1.17", "@tanstack/react-query": "5.95.2", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "convex": "1.35.1", diff --git a/fixtures/vite-auth/src/lib/convex/auth-client.ts b/fixtures/vite-auth/src/lib/convex/auth-client.ts index 3765df65..e4977fc1 100644 --- a/fixtures/vite-auth/src/lib/convex/auth-client.ts +++ b/fixtures/vite-auth/src/lib/convex/auth-client.ts @@ -2,10 +2,31 @@ import { createAuthClient } from 'better-auth/react'; import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; +type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_CONVEX_SITE_URL!, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; export const { useSignInMutationOptions, diff --git a/package.json b/package.json index e69660fc..a6525a42 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@typescript-eslint/parser": "8.56.1", "@typescript/native-preview": "^7.0.0-dev.20260225.1", "@vitest/coverage-v8": "^4.0.18", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "bun-types": "^1.3.9", "concurrently": "^9.2.1", "convex-test": "^0.0.41", diff --git a/packages/kitcn/package.json b/packages/kitcn/package.json index b34d971a..d9881c8e 100644 --- a/packages/kitcn/package.json +++ b/packages/kitcn/package.json @@ -88,7 +88,7 @@ "@tanstack/query-core": ">=5", "@tanstack/react-query": ">=5", "@tanstack/solid-query": ">=5", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "convex": ">=1.35", "hono": "4.12.9", "next": ">=14", diff --git a/packages/kitcn/src/auth-client/convex-auth-provider.tsx b/packages/kitcn/src/auth-client/convex-auth-provider.tsx index 508ece04..794dac28 100644 --- a/packages/kitcn/src/auth-client/convex-auth-provider.tsx +++ b/packages/kitcn/src/auth-client/convex-auth-provider.tsx @@ -27,11 +27,6 @@ import { useAuthValue, } from '../react/auth-store'; -// Re-export AuthClient type -export type { AuthClient } from '@convex-dev/better-auth/react'; - -import type { AuthClient } from '@convex-dev/better-auth/react'; - type AuthClientFetch = AuthClient & { $fetch?: ( path: string, @@ -42,6 +37,49 @@ type AuthClientFetch = AuthClient & { ) => Promise<{ data?: unknown } | null | undefined>; }; +export type AuthClient = { + $store?: { + atoms?: { + session?: { + get?: () => { + data?: unknown; + error?: unknown; + isPending?: boolean; + isRefetching?: boolean; + refetch?: (queryParams?: { + query?: Record; + }) => Promise; + }; + set?: (value: { + data: unknown; + error: unknown; + isPending: boolean; + isRefetching: boolean; + refetch: (queryParams?: { + query?: Record; + }) => Promise; + }) => void; + }; + }; + }; + getSession?: (args?: { + fetchOptions?: { + credentials?: RequestCredentials; + headers?: Record; + }; + }) => Promise<{ data?: unknown } | null | undefined>; + signOut?: (args?: { + fetchOptions?: Record; + }) => Promise; + useSession: () => { + data?: unknown; + error?: unknown; + isPending: boolean; + isRefetching?: boolean; + refetch?: () => Promise; + }; +}; + type IConvexReactClient = { setAuth(fetchToken: AuthTokenFetcher): void; clearAuth(): void; @@ -573,7 +611,7 @@ function useOTTHandler(authClient: AuthClient) { }); const session = result.data?.session; - if (session) { + if (session && typeof authClient.getSession === 'function') { await authClient.getSession({ fetchOptions: { credentials: 'omit', diff --git a/packages/kitcn/src/auth-client/index.ts b/packages/kitcn/src/auth-client/index.ts index bd70bf67..e6481e25 100644 --- a/packages/kitcn/src/auth-client/index.ts +++ b/packages/kitcn/src/auth-client/index.ts @@ -1,3 +1,12 @@ /** biome-ignore-all lint/performance/noBarrelFile: package entry */ -export { convexClient } from '@convex-dev/better-auth/client/plugins'; +import { convexClient as baseConvexClient } from '@convex-dev/better-auth/client/plugins'; +import type { BetterAuthClientPlugin } from 'better-auth'; + +type ConvexClientPlugin = ReturnType & + BetterAuthClientPlugin; + +export const convexClient = ((...args: Parameters) => + baseConvexClient(...args) as ConvexClientPlugin) as ( + ...args: Parameters +) => ConvexClientPlugin; export * from './convex-auth-provider'; diff --git a/packages/kitcn/src/auth/adapter-utils.ts b/packages/kitcn/src/auth/adapter-utils.ts index 2171a541..30a64be2 100644 --- a/packages/kitcn/src/auth/adapter-utils.ts +++ b/packages/kitcn/src/auth/adapter-utils.ts @@ -25,6 +25,7 @@ type AdapterPaginationOptions = PaginationOptions & { export const adapterWhereValidator = v.object({ connector: v.optional(v.union(v.literal('AND'), v.literal('OR'))), field: v.string(), + mode: v.optional(v.union(v.literal('sensitive'), v.literal('insensitive'))), operator: v.optional( v.union( v.literal('lt'), @@ -114,6 +115,7 @@ const findIndex = ( }; where?: { field: string; + mode?: 'sensitive' | 'insensitive'; value: number[] | string[] | boolean | number | string | null; connector?: 'AND' | 'OR'; operator?: @@ -142,6 +144,7 @@ const findIndex = ( const where = args.where?.filter( (w) => + w.mode !== 'insensitive' && (!w.operator || ['eq', 'gt', 'gte', 'in', 'lt', 'lte', 'not_in'].includes( w.operator @@ -365,6 +368,10 @@ const filterByWhere = < const value = doc[w.field as keyof typeof doc] as Infer< typeof adapterWhereValidator >['value']; + const normalizeString = (input: string) => + w.mode === 'insensitive' ? input.toLowerCase() : input; + const normalizeComparable = (input: typeof value) => + typeof input === 'string' ? normalizeString(input) : input; const isLessThan = (val: typeof value, wVal: typeof w.value) => { if (wVal === undefined || wVal === null) { return false; @@ -373,7 +380,10 @@ const filterByWhere = < return true; } - return val < wVal; + return ( + (normalizeComparable(val) as string | number | boolean) < + (normalizeComparable(wVal) as string | number | boolean) + ); }; const isGreaterThan = (val: typeof value, wVal: typeof w.value) => { if (val === undefined || val === null) { @@ -383,19 +393,32 @@ const filterByWhere = < return true; } - return val > wVal; + return ( + (normalizeComparable(val) as string | number | boolean) > + (normalizeComparable(wVal) as string | number | boolean) + ); }; const filter = (w: Infer) => { + const comparableValue = normalizeComparable(value); + const comparableWhereValue = normalizeComparable(w.value); switch (w.operator) { case 'contains': { - return typeof value === 'string' && value.includes(w.value as string); + return ( + typeof comparableValue === 'string' && + typeof comparableWhereValue === 'string' && + comparableValue.includes(comparableWhereValue) + ); } case 'ends_with': { - return typeof value === 'string' && value.endsWith(w.value as string); + return ( + typeof comparableValue === 'string' && + typeof comparableWhereValue === 'string' && + comparableValue.endsWith(comparableWhereValue) + ); } case 'eq': case undefined: { - return value === w.value; + return comparableValue === comparableWhereValue; } case 'gt': { return isGreaterThan(value, w.value); @@ -404,7 +427,12 @@ const filterByWhere = < return value === w.value || isGreaterThan(value, w.value); } case 'in': { - return Array.isArray(w.value) && (w.value as any[]).includes(value); + return ( + Array.isArray(w.value) && + (w.value as any[]).some( + (candidate) => normalizeComparable(candidate) === comparableValue + ) + ); } case 'lt': { return isLessThan(value, w.value); @@ -413,14 +441,21 @@ const filterByWhere = < return value === w.value || isLessThan(value, w.value); } case 'ne': { - return value !== w.value; + return comparableValue !== comparableWhereValue; } case 'not_in': { - return Array.isArray(w.value) && !(w.value as any[]).includes(value); + return ( + Array.isArray(w.value) && + !(w.value as any[]).some( + (candidate) => normalizeComparable(candidate) === comparableValue + ) + ); } case 'starts_with': { return ( - typeof value === 'string' && value.startsWith(w.value as string) + typeof comparableValue === 'string' && + typeof comparableWhereValue === 'string' && + comparableValue.startsWith(comparableWhereValue) ); } } @@ -553,6 +588,7 @@ export const paginate = async < // where clauses as static filters. const uniqueWhere = args.where?.find( (w) => + w.mode !== 'insensitive' && (!w.operator || w.operator === 'eq') && (isUniqueField(betterAuthSchema, args.model, w.field) || w.field === '_id') diff --git a/packages/kitcn/src/auth/index.ts b/packages/kitcn/src/auth/index.ts index 05d0633c..a76f531c 100644 --- a/packages/kitcn/src/auth/index.ts +++ b/packages/kitcn/src/auth/index.ts @@ -2,7 +2,15 @@ * @file Automatically generated by barrelsby. */ -export { convex } from '@convex-dev/better-auth/plugins'; +import { convex as baseConvex } from '@convex-dev/better-auth/plugins'; +import type { BetterAuthPlugin } from 'better-auth'; + +type ConvexPlugin = ReturnType & BetterAuthPlugin; + +export const convex = ((...args: Parameters) => + baseConvex(...args) as ConvexPlugin) as ( + ...args: Parameters +) => ConvexPlugin; export * from './adapter'; export * from './adapter-utils'; export * from './create-api'; diff --git a/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts b/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts index e76ea819..2b9ed287 100644 --- a/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts +++ b/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts @@ -2,10 +2,31 @@ export const AUTH_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-au import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; +type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; export const { useSignInMutationOptions, @@ -18,10 +39,31 @@ export const AUTH_REACT_CLIENT_TEMPLATE = `import { createAuthClient } from 'bet import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; +type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_CONVEX_SITE_URL!, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; export const { useSignInMutationOptions, @@ -34,13 +76,34 @@ export const AUTH_START_CLIENT_TEMPLATE = `import { createAuthClient } from 'bet import { convexClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; +type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: typeof window === 'undefined' ? (import.meta.env.VITE_SITE_URL as string | undefined) : window.location.origin, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; export const { useSignInMutationOptions, @@ -52,17 +115,41 @@ export const { export const AUTH_CONVEX_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; import { convexClient } from 'kitcn/auth/client'; +type KitcnAuthClient = ReturnType & { + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; `; export const AUTH_CONVEX_REACT_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; import { convexClient } from 'kitcn/auth/client'; +type KitcnAuthClient = ReturnType & { + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; +}; + export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_SITE_URL!, plugins: [convexClient()], -}); +}) as unknown as KitcnAuthClient; `; diff --git a/packages/kitcn/src/cli/supported-dependencies.test.ts b/packages/kitcn/src/cli/supported-dependencies.test.ts index 522f1a80..dd40bdaa 100644 --- a/packages/kitcn/src/cli/supported-dependencies.test.ts +++ b/packages/kitcn/src/cli/supported-dependencies.test.ts @@ -19,7 +19,7 @@ import { describe('cli/supported-dependencies', () => { test('extracts package names from install specs', () => { expect(getPackageNameFromInstallSpec('convex@1.35.1')).toBe('convex'); - expect(getPackageNameFromInstallSpec('better-auth@1.5.3')).toBe( + expect(getPackageNameFromInstallSpec('better-auth@1.6.5')).toBe( 'better-auth' ); expect(getPackageNameFromInstallSpec('@scope/pkg@1.2.3')).toBe( @@ -73,8 +73,8 @@ describe('cli/supported-dependencies', () => { 'file:/tmp/kitcn-resend.tgz' ); expect( - resolveSupportedDependencyInstallSpec('better-auth@1.5.3', env) - ).toBe('better-auth@1.5.3'); + resolveSupportedDependencyInstallSpec('better-auth@1.6.5', env) + ).toBe('better-auth@1.6.5'); }); test('warns when the app pins an older supported peer dependency', () => { diff --git a/packages/kitcn/src/cli/supported-dependencies.ts b/packages/kitcn/src/cli/supported-dependencies.ts index 430b3aec..3efe8931 100644 --- a/packages/kitcn/src/cli/supported-dependencies.ts +++ b/packages/kitcn/src/cli/supported-dependencies.ts @@ -7,7 +7,7 @@ const VERSION_IN_SPEC_RE = /(\d+)\.(\d+)(?:\.\d+)?/; const PLAIN_VERSION_SPEC_RE = /^[\^~]?v?\d+\.\d+(?:\.\d+)?$/; const UPPER_BOUND_RE = /(?:^|\s)<={0,1}\s*v?(\d+)\.(\d+)(?:\.\d+)?/g; const SUPPORTED_CONVEX_VERSION = '1.35.1'; -const SUPPORTED_BETTER_AUTH_VERSION = '1.5.3'; +const SUPPORTED_BETTER_AUTH_VERSION = '1.6.5'; const SUPPORTED_HONO_VERSION = '4.12.9'; const SUPPORTED_OPENTELEMETRY_API_VERSION = '1.9.0'; const SUPPORTED_TANSTACK_REACT_QUERY_VERSION = '5.95.2'; diff --git a/packages/kitcn/src/react/auth-mutations.ts b/packages/kitcn/src/react/auth-mutations.ts index e5453a15..43e05742 100644 --- a/packages/kitcn/src/react/auth-mutations.ts +++ b/packages/kitcn/src/react/auth-mutations.ts @@ -92,7 +92,24 @@ const seedReturnedToken = (store: AuthStore, value: unknown) => { } }; -type AnyFn = (...args: any[]) => Promise; +type AnyFn = (...args: unknown[]) => Promise; +type AuthResponse = { + data?: unknown; + error?: { + code?: string; + message?: string; + status?: number; + statusText?: string; + }; +}; + +const toAuthMutationError = (error: AuthResponse['error']) => + new AuthMutationError({ + code: error?.code, + message: error?.message, + status: error?.status ?? 500, + statusText: error?.statusText ?? 'AUTH_ERROR', + }); type MutationArgsWithFetchOptions = { fetchOptions?: Record; }; @@ -123,13 +140,14 @@ type AuthClient = { }; }; getSession?: AnyFn; - signOut: AnyFn; - signIn: { - social: AnyFn; - email: AnyFn; + signOut?: AnyFn; + signIn?: { + anonymous?: AnyFn; + social?: AnyFn; + email?: AnyFn; }; - signUp: { - email: AnyFn; + signUp?: { + email?: AnyFn; }; }; @@ -161,14 +179,14 @@ const hydrateReturnedSession = async ( return; } - const session = await authClient.getSession({ + const session = (await authClient.getSession({ fetchOptions: { credentials: 'omit', headers: { Authorization: `Bearer ${token}`, }, }, - }); + })) as AuthResponse; if (session?.data) { syncSessionAtom(authClient, session.data); @@ -193,24 +211,11 @@ const withDisabledSessionSignal = ( } as T & MutationArgsWithFetchOptions; }; -type AuthMutationsResult = { - useSignOutMutationOptions: MutationOptionsHook< - Awaited>, - // biome-ignore lint/suspicious/noConfusingVoidType: allows mutate() or mutate(options) - Parameters[0] | void - >; - useSignInSocialMutationOptions: MutationOptionsHook< - Awaited>, - Parameters[0] - >; - useSignInMutationOptions: MutationOptionsHook< - Awaited>, - Parameters[0] - >; - useSignUpMutationOptions: MutationOptionsHook< - Awaited>, - Parameters[0] - >; +type AuthMutationsResult = { + useSignOutMutationOptions: MutationOptionsHook; + useSignInSocialMutationOptions: MutationOptionsHook; + useSignInMutationOptions: MutationOptionsHook; + useSignUpMutationOptions: MutationOptionsHook; }; /** @@ -236,23 +241,26 @@ type AuthMutationsResult = { * })); * ``` */ -export function createAuthMutations( - authClient: T -): AuthMutationsResult { +export function createAuthMutations( + authClient: AuthClient +): AuthMutationsResult { const useSignOutMutationOptions = ((options) => { const convexQueryClient = useConvexQueryClient(); const authStoreApi = useAuthStore(); return { ...options, - mutationFn: async (args?: Parameters[0]) => { + mutationFn: async (args?: unknown) => { + if (typeof authClient.signOut !== 'function') { + throw new Error('Auth client does not expose signOut'); + } // Set isAuthenticated: false BEFORE unsubscribing to prevent re-subscriptions // (cache events check shouldSkipSubscription which reads isAuthenticated) authStoreApi.set('isAuthenticated', false); convexQueryClient?.unsubscribeAuthQueries(); - const res = await authClient.signOut(args); + const res = (await authClient.signOut(args)) as AuthResponse; if (res?.error) { - throw new AuthMutationError(res.error); + throw toAuthMutationError(res.error); } authStoreApi.set('token', null); authStoreApi.set('expiresAt', null); @@ -262,7 +270,7 @@ export function createAuthMutations( return res; }, }; - }) as AuthMutationsResult['useSignOutMutationOptions']; + }) as AuthMutationsResult['useSignOutMutationOptions']; const useSignInSocialMutationOptions = ((options) => { const authStoreApi = useAuthStore(); @@ -270,12 +278,15 @@ export function createAuthMutations( return { ...options, - mutationFn: async (args: Parameters[0]) => { - const res = await authClient.signIn.social( + mutationFn: async (args: unknown) => { + if (typeof authClient.signIn?.social !== 'function') { + throw new Error('Auth client does not expose signIn.social'); + } + const res = (await authClient.signIn.social( withDisabledSessionSignal(args) - ); + )) as AuthResponse; if (res?.error) { - throw new AuthMutationError(res.error); + throw toAuthMutationError(res.error); } seedReturnedToken(authStoreApi, res); await hydrateReturnedSession(authClient, res); @@ -285,7 +296,7 @@ export function createAuthMutations( return res; }, }; - }) as AuthMutationsResult['useSignInSocialMutationOptions']; + }) as AuthMutationsResult['useSignInSocialMutationOptions']; const useSignInMutationOptions = ((options) => { const authStoreApi = useAuthStore(); @@ -293,12 +304,15 @@ export function createAuthMutations( return { ...options, - mutationFn: async (args: Parameters[0]) => { - const res = await authClient.signIn.email( + mutationFn: async (args: unknown) => { + if (typeof authClient.signIn?.email !== 'function') { + throw new Error('Auth client does not expose signIn.email'); + } + const res = (await authClient.signIn.email( withDisabledSessionSignal(args) - ); + )) as AuthResponse; if (res?.error) { - throw new AuthMutationError(res.error); + throw toAuthMutationError(res.error); } seedReturnedToken(authStoreApi, res); await hydrateReturnedSession(authClient, res); @@ -308,7 +322,7 @@ export function createAuthMutations( return res; }, }; - }) as AuthMutationsResult['useSignInMutationOptions']; + }) as AuthMutationsResult['useSignInMutationOptions']; const useSignUpMutationOptions = ((options) => { const authStoreApi = useAuthStore(); @@ -316,12 +330,15 @@ export function createAuthMutations( return { ...options, - mutationFn: async (args: Parameters[0]) => { - const res = await authClient.signUp.email( + mutationFn: async (args: unknown) => { + if (typeof authClient.signUp?.email !== 'function') { + throw new Error('Auth client does not expose signUp.email'); + } + const res = (await authClient.signUp.email( withDisabledSessionSignal(args) - ); + )) as AuthResponse; if (res?.error) { - throw new AuthMutationError(res.error); + throw toAuthMutationError(res.error); } seedReturnedToken(authStoreApi, res); await hydrateReturnedSession(authClient, res); @@ -331,7 +348,7 @@ export function createAuthMutations( return res; }, }; - }) as AuthMutationsResult['useSignUpMutationOptions']; + }) as AuthMutationsResult['useSignUpMutationOptions']; return { useSignOutMutationOptions, diff --git a/packages/kitcn/src/solid/types.ts b/packages/kitcn/src/solid/types.ts index fb3286e5..fea2150f 100644 --- a/packages/kitcn/src/solid/types.ts +++ b/packages/kitcn/src/solid/types.ts @@ -1,12 +1,8 @@ -import type { - convexClient, - crossDomainClient, -} from '@convex-dev/better-auth/client/plugins'; import type { BetterAuthClientPlugin } from 'better-auth'; import type { createAuthClient } from 'better-auth/solid'; -type CrossDomainClient = ReturnType; -type ConvexClient = ReturnType; +type CrossDomainClient = BetterAuthClientPlugin; +type ConvexClient = BetterAuthClientPlugin; type PluginsWithCrossDomain = ( | CrossDomainClient | ConvexClient diff --git a/www/content/docs/nextjs/index.mdx b/www/content/docs/nextjs/index.mdx index 9ff323c3..2b5ceb52 100644 --- a/www/content/docs/nextjs/index.mdx +++ b/www/content/docs/nextjs/index.mdx @@ -26,7 +26,7 @@ Setting up kitcn with Next.js involves these components: First, install the required packages: - + }> **Note:** Complete [React Setup](/docs/react) first to create your `query-client.ts` with `hydrationConfig`. From 3546e40ff9e3ce3d1bc6523f1bdd4ecad827f79d Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 18:00:30 +0200 Subject: [PATCH 05/14] fix: resolve sync-convex-auth review feedback --- .agents/rules/sync-convex-auth.mdc | 15 +++++++++++++-- .agents/skills/sync-convex-auth/SKILL.md | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.agents/rules/sync-convex-auth.mdc b/.agents/rules/sync-convex-auth.mdc index c7ff42c6..382d95be 100644 --- a/.agents/rules/sync-convex-auth.mdc +++ b/.agents/rules/sync-convex-auth.mdc @@ -9,7 +9,7 @@ Handle $ARGUMENTS. Goal: compare `https://github.com/zbeyens/convex-better-auth` with its upstream fork, extract every upstream change that matters to kitcn, then delegate one coherent implementation slice to -[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) so it +[$task](../skills/task/SKILL.md) so it opens the PR. ## Rules @@ -41,6 +41,17 @@ gh repo view zbeyens/convex-better-auth \ If `parent` is missing or ambiguous, stop and ask for the upstream repo. +Before asking, try these fallbacks in order: + +```bash +npm view @convex-dev/better-auth repository homepage --json +test -d ../convex-better-auth/.git && git -C ../convex-better-auth remote -v +gh repo view get-convex/better-auth --json nameWithOwner,defaultBranchRef,url +``` + +If npm metadata or the local clone clearly point to one upstream repo, use that +repo and continue instead of stopping. + Use a local clone for navigation, creating it only if missing: ```bash @@ -204,7 +215,7 @@ a slow upstream e2e suite unless the user explicitly approves it. ## 6. Delegate Through `task` Load -[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) with a +[$task](../skills/task/SKILL.md) with a prompt in this exact shape: ```md diff --git a/.agents/skills/sync-convex-auth/SKILL.md b/.agents/skills/sync-convex-auth/SKILL.md index 99f20005..0c5da805 100644 --- a/.agents/skills/sync-convex-auth/SKILL.md +++ b/.agents/skills/sync-convex-auth/SKILL.md @@ -13,7 +13,7 @@ Handle $ARGUMENTS. Goal: compare `https://github.com/zbeyens/convex-better-auth` with its upstream fork, extract every upstream change that matters to kitcn, then delegate one coherent implementation slice to -[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) so it +[$task](../skills/task/SKILL.md) so it opens the PR. ## Rules @@ -45,6 +45,17 @@ gh repo view zbeyens/convex-better-auth \ If `parent` is missing or ambiguous, stop and ask for the upstream repo. +Before asking, try these fallbacks in order: + +```bash +npm view @convex-dev/better-auth repository homepage --json +test -d ../convex-better-auth/.git && git -C ../convex-better-auth remote -v +gh repo view get-convex/better-auth --json nameWithOwner,defaultBranchRef,url +``` + +If npm metadata or the local clone clearly point to one upstream repo, use that +repo and continue instead of stopping. + Use a local clone for navigation, creating it only if missing: ```bash @@ -208,7 +219,7 @@ a slow upstream e2e suite unless the user explicitly approves it. ## 6. Delegate Through `task` Load -[$task](/Users/zbeyens/git/better-convex/.agents/skills/task/SKILL.md) with a +[$task](../skills/task/SKILL.md) with a prompt in this exact shape: ```md From 85d34f9f3e2ea2db15e71ca12cdd4abf59034a7f Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 18:05:03 +0200 Subject: [PATCH 06/14] Support better-auth 1.6.5 --- bun.lock | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index 68c9d42f..a3c48d9a 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ "@typescript-eslint/parser": "8.56.1", "@typescript/native-preview": "^7.0.0-dev.20260225.1", "@vitest/coverage-v8": "^4.0.18", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "bun-types": "^1.3.9", "concurrently": "^9.2.1", "convex-test": "^0.0.41", @@ -70,7 +70,7 @@ "@react-email/render": "2.0.4", "@t3-oss/env-nextjs": "0.13.10", "@tanstack/react-query": "5.95.2", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", @@ -154,7 +154,7 @@ "@tanstack/query-core": ">=5", "@tanstack/react-query": ">=5", "@tanstack/solid-query": ">=5", - "better-auth": "1.5.3", + "better-auth": "1.6.5", "convex": ">=1.35", "hono": "4.12.9", "next": ">=14", @@ -275,15 +275,21 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@better-auth/core": ["@better-auth/core@1.5.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "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" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-fORsQjNZ6BQ7o96xMe7elz3Y4Y8DsqXmQrdyzt289G9rmzX4auwBCPTtE2cXTRTYGiVvH9bv0b97t1Uo/OWynQ=="], + "@better-auth/core": ["@better-auth/core@1.6.5", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-T3u4rVsJcMWShG2qfQUlU1HdkQGLYX0+lcR48QV2Cp2kpBOLOTYdt+p6zZtGm2Omx/ReEouRQyKy7pYtahRQuA=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.3", "", { "peerDependencies": { "@better-auth/core": "1.5.3", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-eAm1KPrlPXkH/qXUXnGBcHPDgCX153b6BSlc2QJ2IeqmiWym9D/6XORqBIZOl71JiP0Cifzocr2GLpnz0gt31Q=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.5", "", { "peerDependencies": { "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-9YjPW35+h66D+QA+YqEJ9pFP97ClLFR+QrTPZojkeP0PTYqpW0ErBK3p1pwRTJG88yK+o3Y4yOwoacMTBxz0jQ=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.3", "", { "peerDependencies": { "@better-auth/core": "1.5.3", "@better-auth/utils": "^0.3.0" } }, "sha512-QdeTI3bvUmaPkHsjcSMfroXyuGsgnxobv7wZVl57e+ox6yQVR1j4VKbqmCILP6PL6Rr2gpcBH/liHr8v5gqY5Q=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.5", "", { "peerDependencies": { "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-kbevd70qzKNR3ZHF7q6/e0XXYRCXanLB2rvmTd3T8WbNEd9kYMqKjgTGNxL1ri5N+PEDUK6zfHx/HrvaEOfoHw=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.5.3", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.3" } }, "sha512-ZX/r8AsWdB6BwH+Rb7H/SyJnGtPN6EDWrNxBQEDsqRrBJVcDLwAIz165P57RXci0WwtY872T0guKq+XVyy5rkA=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.5", "", { "peerDependencies": { "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0" } }, "sha512-5qFUpSdQi+RwHSmNyHMSsJIrFjed8d/ASS61L2xyW7sjBLTIuR7JcgS6hif5cQbtPeq+Qz+Wct5q8oKw33qyqQ=="], - "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.5", "", { "peerDependencies": { "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-HvOUFTiSEFSGTzL/vE3FntTwQiZ79O/V+QcsCimR+65Bj3tOqdFaC1G2Yd1dQ9l2YHNXA9SNBrGekbk66RzJMw=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.5", "", { "peerDependencies": { "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-d7PUO5XoimYYDEG/DoYVbOSbyVYJBDuZgvY9pjf8INccBTCD1BzcyEJ9NQil4huXWj4fcNaGOt2FG0OI8NtWOA=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.5", "", { "peerDependencies": { "@better-auth/core": "^1.6.5", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-Ag3CjAP+tLretKPq+pYdU/gU4pFIcey/AoNQzw671wV5JQZXrMitS65INi8j8QuYfol2xgQrht5KVlcxGrkhHQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], @@ -611,6 +617,8 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="], "@oxc-project/types": ["@oxc-project/types@0.112.0", "", {}, "sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ=="], @@ -1137,9 +1145,9 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], - "better-auth": ["better-auth@1.5.3", "", { "dependencies": { "@better-auth/core": "1.5.3", "@better-auth/kysely-adapter": "1.5.3", "@better-auth/memory-adapter": "1.5.3", "@better-auth/telemetry": "1.5.3", "@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", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/drizzle-adapter": "1.5.3", "@better-auth/mongo-adapter": "1.5.3", "@better-auth/prisma-adapter": "1.5.3", "@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" }, "optionalPeers": ["@better-auth/drizzle-adapter", "@better-auth/mongo-adapter", "@better-auth/prisma-adapter", "@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-E+9kA9GMX1+gT3FfMCqRz0NufT4X/+tNhpOsHW1jLmyPZKinkHtfZkUffSBnG5qGkvfBaH/slT5c1fKttnmF5w=="], + "better-auth": ["better-auth@1.6.5", "", { "dependencies": { "@better-auth/core": "1.6.5", "@better-auth/drizzle-adapter": "1.6.5", "@better-auth/kysely-adapter": "1.6.5", "@better-auth/memory-adapter": "1.6.5", "@better-auth/mongo-adapter": "1.6.5", "@better-auth/prisma-adapter": "1.6.5", "@better-auth/telemetry": "1.6.5", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "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.45.2", "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" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-rSt8JtJOJK0MqPShXINCmM6DV30GsDvnCTlIxQIzP9OpUx/umA40nUc4ALZHQyqAPbw1ib/a549kIWw/WyxxKA=="], - "better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], @@ -1619,7 +1627,7 @@ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "kysely": ["kysely@0.28.12", "", {}, "sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw=="], + "kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="], "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], From 3c7cc2e78a3c9576717340faf5eb1eeeddcff195 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 18:52:21 +0200 Subject: [PATCH 07/14] refactor: centralize auth client compatibility type --- example/src/lib/convex/auth-client.ts | 32 +----- fixtures/next-auth/lib/convex/auth-client.ts | 23 +---- .../start-auth/src/lib/convex/auth-client.ts | 23 +---- .../vite-auth/src/lib/convex/auth-client.ts | 23 +---- packages/kitcn/src/auth-client/index.ts | 34 +++++++ .../items/auth/auth-client.template.ts | 97 +------------------ 6 files changed, 46 insertions(+), 186 deletions(-) diff --git a/example/src/lib/convex/auth-client.ts b/example/src/lib/convex/auth-client.ts index 8906b00a..96adf97f 100644 --- a/example/src/lib/convex/auth-client.ts +++ b/example/src/lib/convex/auth-client.ts @@ -6,41 +6,17 @@ import { organizationClient, } from 'better-auth/client/plugins'; import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; import { env } from '@/env'; -type ExampleAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { +type ExampleAuthClient = KitcnAuthClient & { + signIn: KitcnAuthClient['signIn'] & { anonymous: (args?: unknown) => Promise; - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; }; useActiveOrganization: () => unknown; useListOrganizations: () => unknown; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; - organization: { - checkRolePermission: (args: { - permissions: unknown; - role?: string | null; - }) => unknown; - listMembers: (args: unknown) => Promise<{ - error?: { message?: string }; - }>; - }; + organization: NonNullable; }; export const authClient = createAuthClient({ diff --git a/fixtures/next-auth/lib/convex/auth-client.ts b/fixtures/next-auth/lib/convex/auth-client.ts index ccefcd7c..c5d3f23b 100644 --- a/fixtures/next-auth/lib/convex/auth-client.ts +++ b/fixtures/next-auth/lib/convex/auth-client.ts @@ -1,28 +1,7 @@ import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; -type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; - }; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; - export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, plugins: [convexClient()], diff --git a/fixtures/start-auth/src/lib/convex/auth-client.ts b/fixtures/start-auth/src/lib/convex/auth-client.ts index 612b6762..1ef3c331 100644 --- a/fixtures/start-auth/src/lib/convex/auth-client.ts +++ b/fixtures/start-auth/src/lib/convex/auth-client.ts @@ -1,28 +1,7 @@ import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; -type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; - }; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; - export const authClient = createAuthClient({ baseURL: typeof window === 'undefined' diff --git a/fixtures/vite-auth/src/lib/convex/auth-client.ts b/fixtures/vite-auth/src/lib/convex/auth-client.ts index e4977fc1..dc6d3579 100644 --- a/fixtures/vite-auth/src/lib/convex/auth-client.ts +++ b/fixtures/vite-auth/src/lib/convex/auth-client.ts @@ -1,28 +1,7 @@ import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; -type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; - }; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; - export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_CONVEX_SITE_URL!, plugins: [convexClient()], diff --git a/packages/kitcn/src/auth-client/index.ts b/packages/kitcn/src/auth-client/index.ts index e6481e25..5f7a2605 100644 --- a/packages/kitcn/src/auth-client/index.ts +++ b/packages/kitcn/src/auth-client/index.ts @@ -1,10 +1,44 @@ /** biome-ignore-all lint/performance/noBarrelFile: package entry */ import { convexClient as baseConvexClient } from '@convex-dev/better-auth/client/plugins'; import type { BetterAuthClientPlugin } from 'better-auth'; +import type { createAuthClient } from 'better-auth/react'; type ConvexClientPlugin = ReturnType & BetterAuthClientPlugin; +export type KitcnAuthClient = ReturnType & { + getSession: (args?: unknown) => Promise; + signOut: (args?: unknown) => Promise; + signIn: { + anonymous?: (args?: unknown) => Promise; + email: (args?: unknown) => Promise; + social: (args?: unknown) => Promise; + }; + signUp: { + email: (args?: unknown) => Promise; + }; + useActiveOrganization?: () => unknown; + useListOrganizations?: () => unknown; + useSession: () => { + data?: { + user?: { + email?: string | null; + name?: string | null; + } | null; + } | null; + isPending: boolean; + }; + organization?: { + checkRolePermission: (args: { + permissions: unknown; + role?: string | null; + }) => unknown; + listMembers: (args: unknown) => Promise<{ + error?: { message?: string }; + }>; + }; +}; + export const convexClient = ((...args: Parameters) => baseConvexClient(...args) as ConvexClientPlugin) as ( ...args: Parameters diff --git a/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts b/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts index 2b9ed287..6f9d4b05 100644 --- a/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts +++ b/packages/kitcn/src/cli/registry/items/auth/auth-client.template.ts @@ -1,28 +1,7 @@ export const AUTH_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; -type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; - }; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; - export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, plugins: [convexClient()], @@ -36,30 +15,9 @@ export const { `; export const AUTH_REACT_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; -type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; - }; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; - export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_CONVEX_SITE_URL!, plugins: [convexClient()], @@ -73,30 +31,9 @@ export const { `; export const AUTH_START_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; -type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; - signIn: { - email: (args?: unknown) => Promise; - social: (args?: unknown) => Promise; - }; - signUp: { - email: (args?: unknown) => Promise; - }; - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; - export const authClient = createAuthClient({ baseURL: typeof window === 'undefined' @@ -113,19 +50,7 @@ export const { `; export const AUTH_CONVEX_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; - -type KitcnAuthClient = ReturnType & { - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, @@ -134,19 +59,7 @@ export const authClient = createAuthClient({ `; export const AUTH_CONVEX_REACT_CLIENT_TEMPLATE = `import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; - -type KitcnAuthClient = ReturnType & { - useSession: () => { - data?: { - user?: { - email?: string | null; - name?: string | null; - } | null; - } | null; - isPending: boolean; - }; -}; +import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_SITE_URL!, From 20ae262dd5b69862e5dae321f65b5b41a359a6b7 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 20:38:36 +0200 Subject: [PATCH 08/14] fix: preserve insensitive auth adapter filters --- .changeset/convex-135-agentic-bootstrap.md | 2 +- packages/kitcn/src/auth/adapter-utils.test.ts | 150 ++++++++++++++++++ packages/kitcn/src/auth/adapter-utils.ts | 19 ++- 3 files changed, 164 insertions(+), 7 deletions(-) diff --git a/.changeset/convex-135-agentic-bootstrap.md b/.changeset/convex-135-agentic-bootstrap.md index d3097264..be3d7957 100644 --- a/.changeset/convex-135-agentic-bootstrap.md +++ b/.changeset/convex-135-agentic-bootstrap.md @@ -44,6 +44,6 @@ bun add better-auth@1.6.5 - Warn when an app pins an older Convex dependency family than kitcn expects. - Support Convex `dev --start` as a pre-run conflict flag. - Improve auth route registration so default Convex auth routes avoid eager Better Auth initialization during startup. -- Fix Better Auth adapter index matching for composite equality-plus-sort queries. +- Fix Better Auth adapter index matching and static filtering for composite and case-insensitive queries. - Support `@convex-dev/better-auth@0.11.4`. - Support the Better Auth 1.6 client surface in generated auth clients and providers. diff --git a/packages/kitcn/src/auth/adapter-utils.test.ts b/packages/kitcn/src/auth/adapter-utils.test.ts index 36e9e310..4ec82388 100644 --- a/packages/kitcn/src/auth/adapter-utils.test.ts +++ b/packages/kitcn/src/auth/adapter-utils.test.ts @@ -52,6 +52,156 @@ describe('selectFields', () => { }); describe('paginate', () => { + test('reapplies insensitive equality filters after index selection', async () => { + const queryBuilder = { + eq: () => queryBuilder, + }; + const db = { + query: () => ({ + withIndex: ( + _indexName: string, + applyRange?: (q: typeof queryBuilder) => unknown + ) => { + applyRange?.(queryBuilder); + + return { + order: () => ({ + async *[Symbol.asyncIterator]() { + yield { + _creationTime: 1, + _id: 'account-1', + email: 'ALICE@example.com', + providerId: 'github', + }; + yield { + _creationTime: 2, + _id: 'account-2', + email: 'bob@example.com', + providerId: 'github', + }; + }, + }), + }; + }, + }), + }; + + const result = await paginate( + { db } as any, + { + tables: { + account: { + indexes: [ + { + fields: ['providerId'], + indexDescriptor: 'by_provider', + }, + ], + export: () => ({ + indexes: [ + { + fields: ['providerId'], + indexDescriptor: 'by_provider', + }, + ], + }), + }, + }, + } as any, + {} as any, + { + model: 'account', + paginationOpts: { cursor: null, numItems: 10 }, + where: [ + { + field: 'providerId', + operator: 'eq', + value: 'github', + }, + { + field: 'email', + mode: 'insensitive', + operator: 'eq', + value: 'alice@example.com', + }, + ], + } + ); + + expect(result.page).toEqual([ + { + _creationTime: 1, + _id: 'account-1', + email: 'ALICE@example.com', + providerId: 'github', + }, + ]); + }); + + test('matches insensitive range boundaries with different casing', async () => { + const db = { + query: () => ({ + withIndex: () => ({ + order: () => ({ + async *[Symbol.asyncIterator]() { + yield { + _creationTime: 1, + _id: 'account-1', + email: 'ABC@example.com', + }; + yield { + _creationTime: 2, + _id: 'account-2', + email: 'abd@example.com', + }; + }, + }), + }), + }), + }; + + const result = await paginate( + { db } as any, + { + tables: { + account: { + indexes: [], + export: () => ({ + indexes: [], + }), + }, + }, + } as any, + {} as any, + { + model: 'account', + paginationOpts: { cursor: null, numItems: 10 }, + where: [ + { + field: 'email', + mode: 'insensitive', + operator: 'gte', + value: 'abc@example.com', + }, + { + field: 'email', + mode: 'insensitive', + operator: 'lte', + value: 'abc@example.com', + }, + ], + } + ); + + expect(result.page).toEqual([ + { + _creationTime: 1, + _id: 'account-1', + email: 'ABC@example.com', + }, + ]); + }); + test('uses composite indexes with real field names for eq plus sortBy', async () => { const indexCalls: Array<{ indexName: string }> = []; const rangeCalls: Array<{ field: string; value: unknown }> = []; diff --git a/packages/kitcn/src/auth/adapter-utils.ts b/packages/kitcn/src/auth/adapter-utils.ts index 30a64be2..6ab93946 100644 --- a/packages/kitcn/src/auth/adapter-utils.ts +++ b/packages/kitcn/src/auth/adapter-utils.ts @@ -424,7 +424,10 @@ const filterByWhere = < return isGreaterThan(value, w.value); } case 'gte': { - return value === w.value || isGreaterThan(value, w.value); + return ( + comparableValue === comparableWhereValue || + isGreaterThan(value, w.value) + ); } case 'in': { return ( @@ -438,7 +441,10 @@ const filterByWhere = < return isLessThan(value, w.value); } case 'lte': { - return value === w.value || isLessThan(value, w.value); + return ( + comparableValue === comparableWhereValue || + isLessThan(value, w.value) + ); } case 'ne': { return comparableValue !== comparableWhereValue; @@ -538,10 +544,11 @@ const generateQuery = ( // Index used for all eq and range clauses, apply remaining clauses // incompatible with Convex statically. (w) => - w.operator && - ['contains', 'ends_with', 'ne', 'not_in', 'starts_with'].includes( - w.operator - ) + w.mode === 'insensitive' || + (w.operator && + ['contains', 'ends_with', 'ne', 'not_in', 'starts_with'].includes( + w.operator + )) ); }); From f83601b54e8827be83f8408446d8c4bba9e6e44b Mon Sep 17 00:00:00 2001 From: zbeyens Date: Thu, 16 Apr 2026 21:30:33 +0200 Subject: [PATCH 09/14] Remove convex-better-auth dependency and fork auth helpers --- .changeset/convex-135-agentic-bootstrap.md | 5 +- bun.lock | 10 +- ...tructural-convex-auth-wrappers-20260416.md | 21 +- fixtures/next-auth/lib/convex/auth-client.ts | 5 +- .../start-auth/src/lib/convex/auth-client.ts | 5 +- .../vite-auth/src/lib/convex/auth-client.ts | 5 +- package.json | 1 - packages/kitcn/package.json | 3 +- .../skills/convex/references/features/auth.md | 3 +- .../skills/convex/references/setup/index.md | 2 +- .../skills/convex/references/setup/react.md | 3 +- .../skills/convex/references/setup/start.md | 3 +- .../src/auth-client/convex-auth-provider.tsx | 15 +- packages/kitcn/src/auth-client/index.test.ts | 1 + packages/kitcn/src/auth-client/index.ts | 45 +- packages/kitcn/src/auth-nextjs/index.test.ts | 12 + packages/kitcn/src/auth-nextjs/index.ts | 16 +- packages/kitcn/src/auth-start/index.test.ts | 45 ++ packages/kitcn/src/auth-start/index.ts | 178 +++++++- packages/kitcn/src/auth/index.ts | 2 +- .../kitcn/src/auth/internal/convex-client.ts | 9 + .../kitcn/src/auth/internal/convex-plugin.ts | 424 ++++++++++++++++++ packages/kitcn/src/auth/internal/token.ts | 87 ++++ .../kitcn/src/auth/registerRoutes.test.ts | 37 ++ packages/kitcn/src/auth/registerRoutes.ts | 35 +- .../kitcn/src/cli/registry/dependencies.ts | 3 +- .../items/auth/auth-client.template.test.ts | 26 ++ .../items/auth/auth-client.template.ts | 25 +- .../items/auth/reconcile-auth-schema.test.ts | 8 +- .../items/auth/reconcile-auth-schema.ts | 2 +- packages/kitcn/src/server/caller-factory.ts | 4 +- www/content/docs/auth/client.mdx | 3 +- www/content/docs/auth/plugins/polar.mdx | 2 +- www/content/docs/migrations/auth.mdx | 3 +- www/content/docs/nextjs/index.mdx | 3 +- www/content/docs/tanstack-start.mdx | 3 +- 36 files changed, 960 insertions(+), 94 deletions(-) create mode 100644 packages/kitcn/src/auth/internal/convex-client.ts create mode 100644 packages/kitcn/src/auth/internal/convex-plugin.ts create mode 100644 packages/kitcn/src/auth/internal/token.ts create mode 100644 packages/kitcn/src/cli/registry/items/auth/auth-client.template.test.ts diff --git a/.changeset/convex-135-agentic-bootstrap.md b/.changeset/convex-135-agentic-bootstrap.md index be3d7957..28b1aa80 100644 --- a/.changeset/convex-135-agentic-bootstrap.md +++ b/.changeset/convex-135-agentic-bootstrap.md @@ -44,6 +44,7 @@ bun add better-auth@1.6.5 - Warn when an app pins an older Convex dependency family than kitcn expects. - Support Convex `dev --start` as a pre-run conflict flag. - Improve auth route registration so default Convex auth routes avoid eager Better Auth initialization during startup. +- Preserve forwarded host and protocol headers through Next.js, TanStack Start, and Convex auth route proxies. - Fix Better Auth adapter index matching and static filtering for composite and case-insensitive queries. -- Support `@convex-dev/better-auth@0.11.4`. -- Support the Better Auth 1.6 client surface in generated auth clients and providers. +- Replace the `@convex-dev/better-auth` runtime dependency with internal kitcn auth helpers. +- Support the Better Auth 1.6 client surface through `kitcn/auth/client` without requiring casts in app auth clients. diff --git a/bun.lock b/bun.lock index a3c48d9a..0af6d197 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "": { "name": "kitcn", "dependencies": { - "@convex-dev/better-auth": "0.11.4", "@tanstack/query-core": "5.90.20", "@tanstack/react-query": "5.95.2", "@tanstack/solid-query": "5.90.23", @@ -124,8 +123,8 @@ }, "dependencies": { "@babel/parser": "^7.28.4", + "@better-fetch/fetch": "^1.1.21", "@clack/prompts": "^0.11.0", - "@convex-dev/better-auth": "^0.11.4", "chokidar": "^5.0.0", "common-tags": "^1.8.2", "diff": "^8.0.2", @@ -133,6 +132,7 @@ "esbuild": "^0.27.3", "execa": "^9.6.1", "jiti": "^2.6.1", + "jose": "^6.1.3", "jotai": "^2.18.0", "jotai-x": "^2.3.3", "picocolors": "^1.1.1", @@ -383,8 +383,6 @@ "@concavejs/runtime-bun": ["@concavejs/runtime-bun@0.0.1-alpha.14", "", { "dependencies": { "@concavejs/blobstore-bun-fs": "0.0.1-alpha.14", "@concavejs/blobstore-bun-s3": "0.0.1-alpha.14", "@concavejs/core": "0.0.1-alpha.14", "@concavejs/docstore-bun-sqlite": "0.0.1-alpha.14", "@concavejs/runtime-base": "0.0.1-alpha.14", "convex": "^1.33.1" } }, "sha512-YZihihRHcnyWgeiCcTCrbwzb/jXW79ozhpAV037FySggaKGrWZFfWWx6ekOZlelO+R+sbtpm21+bbgpkiRSpkQ=="], - "@convex-dev/better-auth": ["@convex-dev/better-auth@0.11.4", "", { "dependencies": { "@better-fetch/fetch": "^1.1.18", "common-tags": "^1.8.2", "convex-helpers": "^0.1.95", "jose": "^6.1.0", "remeda": "^2.32.0", "semver": "^7.7.3", "type-fest": "^4.39.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": ">=1.5.0 <1.6.0", "convex": "^1.25.0", "react": "^18.3.1 || ^19.0.0" } }, "sha512-CsRsM7UaQgQKH1mt4z64QOZUfrhZKNsspkpWjlhqLiIz5THfnM2vaSVMQ3GOddcUvw7AgkNCNR3u8C8HjLQEMA=="], - "@convex-dev/react-query": ["@convex-dev/react-query@0.1.0", "", { "peerDependencies": { "@tanstack/react-query": "^5.0.0", "convex": "^1.29.3" } }, "sha512-ULmBZCtAQDUePdBhUv7hj1Yy4QiCelhi6uEIOtMpjW8db6W9CFKvQwLt9heLngccFyMFfAZdOfSrT+cP4AH0Jw=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -1231,8 +1229,6 @@ "convex": ["convex@1.35.1", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "@clerk/react": "^6.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "@clerk/react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-g23KrTjBiXqRHzWIN0PVFagKjrmFxWUaOSiBsAWPTpXX2rXl0L1F4PR0YpAcMJEzMgfZR9AGymJvLTM+KA6lsQ=="], - "convex-helpers": ["convex-helpers@0.1.114", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.32.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-elEdh+gG6BDv2dWIWVvBeJPbHnDQS5+WexUuwlGVJXz1EbMkXz/UIQwFIfLMZIXUwW6ot4JYf/1JJKNStrE6lg=="], - "convex-test": ["convex-test@0.0.41", "", { "peerDependencies": { "convex": "^1.16.4" } }, "sha512-GPHeYFOi70n7UtW0eCEQFVhzl/+m8PvbWkDCbKpHLybI1MrScf4sVpGeM0cC2qmtxiduxa2nLPbehPalhh9oyQ=="], "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], @@ -2379,8 +2375,6 @@ "@concavejs/runtime-bun/convex": ["convex@1.34.0", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-TbC509Z4urZMChZR2aLPgalQ8gMhAYSz2VMxaYsCvba8YqB0Uxma7zWnXwRn7aEGXuA8ro5/uHgD1IJ0HhYYPg=="], - "@convex-dev/better-auth/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], diff --git a/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md b/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md index bbe5e7a7..6012b810 100644 --- a/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md +++ b/docs/solutions/integration-issues/better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md @@ -1,6 +1,7 @@ --- title: Better Auth 1.6 support needs structural Convex auth wrappers date: 2026-04-16 +last_updated: 2026-04-16 category: integration-issues module: auth-client problem_type: integration_issue @@ -19,9 +20,9 @@ tags: [better-auth, auth, convex, wrappers, typecheck] ## Problem -`kitcn` uses `@convex-dev/better-auth` for the Convex plugin and client plugin, -but Better Auth `1.6.x` changed enough of the client and plugin type surface -that the old exported types no longer compose cleanly. +`kitcn` used the Convex Better Auth package for the Convex plugin and client +plugin, but Better Auth `1.6.x` changed enough of the client and plugin type +surface that the old exported types no longer composed cleanly. Runtime behavior was mostly fine. TypeScript was the part that exploded. @@ -39,6 +40,8 @@ Runtime behavior was mostly fine. TypeScript was the part that exploded. - trusting direct re-exports from `@convex-dev/better-auth` - expecting Better Auth `1.6.x` to infer the same client shape through the old Convex plugin types +- casting generated app clients through `as unknown as KitcnAuthClient`; that + moves package compatibility debt into user code ## Solution @@ -55,8 +58,12 @@ Key changes: - do the same for `ConvexAuthProvider` - add a local `mode` field to the auth adapter `where` validator so Better Auth `1.6` queries do not fail validation -- cast generated auth client templates and the example auth client through - `unknown` to a stable local interface with the methods the app actually uses +- re-export a wrapped `createAuthClient` from `kitcn/auth/client` so generated + apps get the stable local interface without user-code casts +- keep the internal compatibility cast inside `kitcn/auth/client` and preserve + plugin-specific Better Auth fields around the structural session/action shape +- vendor the small Convex Better Auth runtime surfaces kitcn uses so package + code no longer imports or depends on `@convex-dev/better-auth` ## Why This Works @@ -64,7 +71,7 @@ The runtime object already had the right methods. The breakage was in the type bridge between: 1. Better Auth `1.6.x` -2. `@convex-dev/better-auth@0.11.4` +2. kitcn's internal Convex auth helpers 3. kitcn's wrappers and generated auth client files By making kitcn depend on structural contracts at the boundaries, we stop @@ -80,6 +87,8 @@ detail before the app can compile. not just the hand-written example app. 3. Re-run `bun typecheck`, `bun run fixtures:check`, and `bun check` after any auth client type widening. The generated apps are the real proof. +4. Never put dependency-compatibility casts in scaffolded app code. Wrap the + unstable dependency boundary in the package API. ## Related Issues diff --git a/fixtures/next-auth/lib/convex/auth-client.ts b/fixtures/next-auth/lib/convex/auth-client.ts index c5d3f23b..9a148401 100644 --- a/fixtures/next-auth/lib/convex/auth-client.ts +++ b/fixtures/next-auth/lib/convex/auth-client.ts @@ -1,11 +1,10 @@ -import { createAuthClient } from 'better-auth/react'; -import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; +import { convexClient, createAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; export const authClient = createAuthClient({ baseURL: process.env.NEXT_PUBLIC_SITE_URL!, plugins: [convexClient()], -}) as unknown as KitcnAuthClient; +}); export const { useSignInMutationOptions, diff --git a/fixtures/start-auth/src/lib/convex/auth-client.ts b/fixtures/start-auth/src/lib/convex/auth-client.ts index 1ef3c331..3ebbfcdd 100644 --- a/fixtures/start-auth/src/lib/convex/auth-client.ts +++ b/fixtures/start-auth/src/lib/convex/auth-client.ts @@ -1,5 +1,4 @@ -import { createAuthClient } from 'better-auth/react'; -import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; +import { convexClient, createAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; export const authClient = createAuthClient({ @@ -8,7 +7,7 @@ export const authClient = createAuthClient({ ? (import.meta.env.VITE_SITE_URL as string | undefined) : window.location.origin, plugins: [convexClient()], -}) as unknown as KitcnAuthClient; +}); export const { useSignInMutationOptions, diff --git a/fixtures/vite-auth/src/lib/convex/auth-client.ts b/fixtures/vite-auth/src/lib/convex/auth-client.ts index dc6d3579..9352c863 100644 --- a/fixtures/vite-auth/src/lib/convex/auth-client.ts +++ b/fixtures/vite-auth/src/lib/convex/auth-client.ts @@ -1,11 +1,10 @@ -import { createAuthClient } from 'better-auth/react'; -import { convexClient, type KitcnAuthClient } from 'kitcn/auth/client'; +import { convexClient, createAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_CONVEX_SITE_URL!, plugins: [convexClient()], -}) as unknown as KitcnAuthClient; +}); export const { useSignInMutationOptions, diff --git a/package.json b/package.json index a6525a42..2c4552ba 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "typecheck:watch": "tsc --noEmit --watch" }, "dependencies": { - "@convex-dev/better-auth": "0.11.4", "@tanstack/query-core": "5.90.20", "@tanstack/react-query": "5.95.2", "@tanstack/solid-query": "5.90.23", diff --git a/packages/kitcn/package.json b/packages/kitcn/package.json index d9881c8e..5c96c384 100644 --- a/packages/kitcn/package.json +++ b/packages/kitcn/package.json @@ -58,8 +58,8 @@ }, "dependencies": { "@babel/parser": "^7.28.4", + "@better-fetch/fetch": "^1.1.21", "@clack/prompts": "^0.11.0", - "@convex-dev/better-auth": "^0.11.4", "chokidar": "^5.0.0", "common-tags": "^1.8.2", "diff": "^8.0.2", @@ -69,6 +69,7 @@ "jiti": "^2.6.1", "jotai": "^2.18.0", "jotai-x": "^2.3.3", + "jose": "^6.1.3", "picocolors": "^1.1.1", "remeda": "^2.33.6", "svix": "^1.84.1", diff --git a/packages/kitcn/skills/convex/references/features/auth.md b/packages/kitcn/skills/convex/references/features/auth.md index a8b0991d..93da3702 100644 --- a/packages/kitcn/skills/convex/references/features/auth.md +++ b/packages/kitcn/skills/convex/references/features/auth.md @@ -284,8 +284,7 @@ Default `definePayload` includes all user fields except `id` and `image`, plus ` ```ts // src/lib/convex/auth-client.ts import { inferAdditionalFields } from 'better-auth/client/plugins'; -import { createAuthClient } from 'better-auth/react'; -import { convexClient } from 'kitcn/auth/client'; +import { convexClient, createAuthClient } from 'kitcn/auth/client'; import { createAuthMutations } from 'kitcn/react'; import type { Auth } from '@convex/auth-shared'; diff --git a/packages/kitcn/skills/convex/references/setup/index.md b/packages/kitcn/skills/convex/references/setup/index.md index 883ae7e4..2e5128f3 100644 --- a/packages/kitcn/skills/convex/references/setup/index.md +++ b/packages/kitcn/skills/convex/references/setup/index.md @@ -772,7 +772,7 @@ This runbook + references map to the canonical template shape as follows: | `Property 'insert'/'update' does not exist on type 'OrmReader'` | Using query context for mutations | Ensure mutation handlers use `publicMutation` / `protectedMutation` builders | | `useCRPC must be used within CRPCProvider` | Provider chain not mounted around route tree | Wrap app with `AppConvexProvider` and verify `CRPCProvider` is inside QueryClientProvider (Section 7.4 / 8.A.4) | | Route auth cookies not set | Missing CORS auth headers | Add `Better-Auth-Cookie` allow/expose headers + credentials | -| TanStack Start auth helper import errors | Using `kitcn/auth/nextjs` in Start app | Use TanStack Start exception with `@convex-dev/better-auth/*` helpers | +| TanStack Start auth helper import errors | Using `kitcn/auth/nextjs` in Start app | Use the TanStack Start helper from `kitcn/auth/start` | | `Returned promise will never resolve` from internal function | Trigger path is recursively querying/updating related rows or stale component wiring still runs | Isolate failing write with logs, disable/move trigger-side sync into explicit mutation helper, rerun `bunx kitcn verify`, then retry the runtime proof | | Better Auth secret mismatch/warnings in setup flows | `BETTER_AUTH_SECRET` manually set inconsistently or low entropy | Let `bunx kitcn dev` manage local bootstrap, or regenerate/push via `bunx kitcn env push` for active non-local targets | | `Invalid orderBy value. Use a column or asc()/desc()` | Wrong `orderBy` shape (`[{ field, direction }]`) | Use object form only, e.g. `orderBy: { updatedAt: "desc" }` | diff --git a/packages/kitcn/skills/convex/references/setup/react.md b/packages/kitcn/skills/convex/references/setup/react.md index a846a62e..310ae323 100644 --- a/packages/kitcn/skills/convex/references/setup/react.md +++ b/packages/kitcn/skills/convex/references/setup/react.md @@ -11,8 +11,7 @@ Prerequisite: ```ts import type { Auth } from "@convex/auth-shared"; import { adminClient, inferAdditionalFields } from "better-auth/client/plugins"; -import { createAuthClient } from "better-auth/react"; -import { convexClient } from "kitcn/auth/client"; +import { convexClient, createAuthClient } from "kitcn/auth/client"; import { createAuthMutations } from "kitcn/react"; export const authClient = createAuthClient({ diff --git a/packages/kitcn/skills/convex/references/setup/start.md b/packages/kitcn/skills/convex/references/setup/start.md index c1acbc8a..8f2ea234 100644 --- a/packages/kitcn/skills/convex/references/setup/start.md +++ b/packages/kitcn/skills/convex/references/setup/start.md @@ -12,8 +12,7 @@ auth-owned schema blocks with `bunx kitcn add auth --schema --yes`. Keep **Create:** `src/lib/convex/auth-client.ts` ```ts -import { createAuthClient } from "better-auth/react"; -import { convexClient } from "kitcn/auth/client"; +import { convexClient, createAuthClient } from "kitcn/auth/client"; import { createAuthMutations } from "kitcn/react"; export const authClient = createAuthClient({ diff --git a/packages/kitcn/src/auth-client/convex-auth-provider.tsx b/packages/kitcn/src/auth-client/convex-auth-provider.tsx index 794dac28..f39b74cd 100644 --- a/packages/kitcn/src/auth-client/convex-auth-provider.tsx +++ b/packages/kitcn/src/auth-client/convex-auth-provider.tsx @@ -67,7 +67,7 @@ export type AuthClient = { credentials?: RequestCredentials; headers?: Record; }; - }) => Promise<{ data?: unknown } | null | undefined>; + }) => Promise; signOut?: (args?: { fetchOptions?: Record; }) => Promise; @@ -120,6 +120,14 @@ const wait = (ms: number) => setTimeout(resolve, ms); }); +const readAuthResultData = (result: unknown) => { + if (!result || typeof result !== 'object') { + return; + } + + return (result as { data?: unknown }).data; +}; + const getSessionFromPersistedToken = async ( authClient: AuthClientFetch, token: string @@ -143,8 +151,9 @@ const getSessionFromPersistedToken = async ( }, }); - if (result?.data) { - return result.data; + const data = readAuthResultData(result); + if (data) { + return data; } if (attempt < 9) { diff --git a/packages/kitcn/src/auth-client/index.test.ts b/packages/kitcn/src/auth-client/index.test.ts index 8d0c85d3..8c406905 100644 --- a/packages/kitcn/src/auth-client/index.test.ts +++ b/packages/kitcn/src/auth-client/index.test.ts @@ -3,6 +3,7 @@ import * as authClient from './index'; describe('auth-client public exports', () => { test('re-exports ConvexAuthProvider surface', () => { expect(typeof authClient.ConvexAuthProvider).toBe('function'); + expect(typeof authClient.createAuthClient).toBe('function'); expect(typeof authClient.convexClient).toBe('function'); }); }); diff --git a/packages/kitcn/src/auth-client/index.ts b/packages/kitcn/src/auth-client/index.ts index 5f7a2605..273bbdea 100644 --- a/packages/kitcn/src/auth-client/index.ts +++ b/packages/kitcn/src/auth-client/index.ts @@ -1,21 +1,33 @@ /** biome-ignore-all lint/performance/noBarrelFile: package entry */ -import { convexClient as baseConvexClient } from '@convex-dev/better-auth/client/plugins'; import type { BetterAuthClientPlugin } from 'better-auth'; -import type { createAuthClient } from 'better-auth/react'; +import type { BetterAuthClientOptions } from 'better-auth/client'; +import { createAuthClient as createBetterAuthClient } from 'better-auth/react'; +import { convexClient as baseConvexClient } from '../auth/internal/convex-client'; type ConvexClientPlugin = ReturnType & BetterAuthClientPlugin; -export type KitcnAuthClient = ReturnType & { - getSession: (args?: unknown) => Promise; - signOut: (args?: unknown) => Promise; +type BetterAuthReactClient