Skip to content

Commit fafaac0

Browse files
authored
Merge pull request #210 from udecode/codex/fix-raw-convex-auth-kitcn-dep
2 parents 7dd1aa0 + 1b3468a commit fafaac0

8 files changed

Lines changed: 190 additions & 60 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"kitcn": patch
3+
---
4+
5+
## Patches
6+
7+
- Fix raw Convex auth adoption so `kitcn add auth --preset convex --yes`
8+
installs `kitcn` before codegen and local bootstrap.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
title: Raw Convex auth adoption must install kitcn runtime before codegen
3+
date: 2026-04-15
4+
category: integration-issues
5+
module: auth-adoption
6+
problem_type: integration_issue
7+
component: tooling
8+
severity: high
9+
symptoms:
10+
- '`kitcn add auth --preset convex --yes` fails during Convex bootstrap with `Could not resolve "kitcn/auth"` or `Could not resolve "kitcn/auth/http"`'
11+
- raw Convex auth adoption writes `convex/auth.ts`, `convex/http.ts`, and `src/lib/convex/auth-client.ts`, but the app `package.json` still lacks `kitcn`'
12+
- local scenario runs with `KITCN_INSTALL_SPEC` can end up with duplicate `kitcn` dependency keys if the hint is pre-resolved too early'
13+
root_cause: missing_tooling
14+
resolution_type: code_fix
15+
tags:
16+
- convex
17+
- auth
18+
- cli
19+
- scaffolding
20+
- package-management
21+
- scenarios
22+
---
23+
24+
# Raw Convex auth adoption must install kitcn runtime before codegen
25+
26+
## Problem
27+
28+
The raw Convex auth preset generated files that import `kitcn/*`, but the
29+
install plan only guaranteed `better-auth` and OpenTelemetry.
30+
31+
That meant `kitcn add auth --preset convex --yes` could scaffold the right
32+
files and then immediately die when Convex tried to bundle them.
33+
34+
## Symptoms
35+
36+
- `convex/auth.ts` imports `kitcn/auth`, but the app has no `kitcn`
37+
dependency
38+
- Convex bootstrap fails before JWKS sync with unresolved `kitcn/*` imports
39+
- local scenario runs can show duplicate `kitcn` keys in `package.json` if the
40+
dependency hint is stored as a pre-resolved tarball path
41+
42+
## What Didn't Work
43+
44+
- treating `better-auth` as the only runtime dependency for the raw preset
45+
- storing the raw preset hint as an already-resolved install spec like
46+
`file:/.../kitcn-0.12.27.tgz`
47+
48+
The second cut was especially sneaky: it fixed published installs, but it
49+
defeated package-name detection during local scenario runs, so the CLI could no
50+
longer tell that `kitcn` was already present.
51+
52+
## Solution
53+
54+
Keep the raw preset dependency hint at the package-name level:
55+
56+
- declare `kitcn` as a raw auth scaffold dependency hint
57+
- resolve that hint to the current package install spec only at install time
58+
59+
That keeps both paths honest:
60+
61+
1. published CLI runs install `kitcn@<current-version>`
62+
2. local scenario runs still collapse to the tarball override from
63+
`KITCN_INSTALL_SPEC`
64+
3. duplicate detection still works because the missing-dependency scan compares
65+
against the raw package name `kitcn`, not a tarball URL
66+
67+
## Why This Works
68+
69+
The raw preset contract is different from the managed kitcn baseline.
70+
71+
Managed apps already depend on `kitcn`, so auth scaffolding can assume the
72+
runtime helpers exist. Raw Convex adoption cannot. It patches a foreign app in
73+
place, so every emitted `kitcn/*` import must be matched by an explicit
74+
dependency install before codegen or local bootstrap runs.
75+
76+
Resolving the install spec too early turns `kitcn` into an opaque file URL,
77+
which breaks the package-name check that prevents duplicate installs.
78+
79+
## Prevention
80+
81+
1. If a preset emits `kitcn/*` imports into an app that did not come from
82+
`kitcn init`, treat `kitcn` as an explicit scaffold dependency.
83+
2. Keep dependency hints as package-name specs until install time. Resolve
84+
local tarball overrides as late as possible.
85+
3. Keep `raw-start-auth-adoption` in the scenario gate. This bug is easy to
86+
miss in file-only tests and obvious in the real bootstrap lane.
87+
88+
## Related Issues
89+
90+
- `docs/solutions/integration-issues/raw-convex-auth-adoption-bootstrap-20260318.md`
91+
- `docs/solutions/integration-issues/raw-convex-start-auth-adoption-must-patch-start-provider-and-react-client-20260410.md`

packages/kitcn/src/cli/backend-core.ts

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,11 @@ import {
128128
BASELINE_DEPENDENCY_INSTALL_SPECS,
129129
getPackageNameFromInstallSpec,
130130
INIT_TEMPLATE_DEPENDENCY_INSTALL_SPECS,
131-
KITCN_INSTALL_SPEC_ENV,
131+
resolveScaffoldInstallSpec,
132132
} from './supported-dependencies.js';
133+
134+
export { resolveScaffoldInstallSpec } from './supported-dependencies.js';
135+
133136
import { isContentEquivalent } from './utils/content-compare.js';
134137
import { CRPC_BUILDER_STUB_SOURCE } from './utils/crpc-builder-stub.js';
135138
import {
@@ -146,7 +149,6 @@ import { createTypeScriptProxy } from './utils/typescript-runtime.js';
146149

147150
const __filename = fileURLToPath(import.meta.url);
148151
const __dirname = dirname(__filename);
149-
let ownVersion: string | undefined | null;
150152
const ts = createTypeScriptProxy();
151153

152154
// Resolve real convex CLI binary
@@ -796,54 +798,6 @@ export function createCommandEnv(
796798
};
797799
}
798800

799-
function resolveOwnPackageJsonPath(filePath: string): string {
800-
let current = dirname(filePath);
801-
802-
while (true) {
803-
const candidate = join(current, 'package.json');
804-
if (fs.existsSync(candidate)) {
805-
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8')) as {
806-
name?: string;
807-
version?: string;
808-
};
809-
if (parsed.name === 'kitcn') {
810-
return candidate;
811-
}
812-
}
813-
814-
const parent = dirname(current);
815-
if (parent === current) {
816-
throw new Error(`Could not find kitcn package.json from ${filePath}.`);
817-
}
818-
current = parent;
819-
}
820-
}
821-
822-
function readOwnVersion() {
823-
if (ownVersion !== undefined) {
824-
return ownVersion ?? undefined;
825-
}
826-
827-
const packageJsonPath = resolveOwnPackageJsonPath(__filename);
828-
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
829-
version?: string;
830-
};
831-
ownVersion = parsed.version ?? null;
832-
return ownVersion ?? undefined;
833-
}
834-
835-
export function resolveScaffoldInstallSpec(
836-
env: Record<string, string | undefined> = process.env
837-
) {
838-
const override = env[KITCN_INSTALL_SPEC_ENV]?.trim();
839-
if (override) {
840-
return override;
841-
}
842-
843-
const version = readOwnVersion();
844-
return version ? `kitcn@${version}` : 'kitcn';
845-
}
846-
847801
const CONVEX_DEPLOYMENT_ENV_KEYS = [
848802
'CONVEX_DEPLOYMENT',
849803
'CONVEX_DEPLOY_KEY',

packages/kitcn/src/cli/cli.commands.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fs from 'node:fs';
22
import os from 'node:os';
33
import path from 'node:path';
4-
import { resolveScaffoldInstallSpec } from './backend-core';
54
import {
65
collectPluginScaffoldTemplates,
76
ensureConvexGitignoreEntry,
@@ -19,7 +18,11 @@ import {
1918
} from './commands/init';
2019
import { getPluginCatalogEntry } from './registry/index';
2120
import { RESEND_SCHEMA_TEMPLATE } from './registry/items/resend/resend-schema.template';
22-
import { BETTER_AUTH_INSTALL_SPEC } from './supported-dependencies';
21+
import {
22+
BETTER_AUTH_INSTALL_SPEC,
23+
OPENTELEMETRY_API_INSTALL_SPEC,
24+
resolveScaffoldInstallSpec,
25+
} from './supported-dependencies';
2326
import {
2427
writeShadcnNextApp,
2528
writeShadcnStartApp,
@@ -2573,6 +2576,10 @@ describe('cli/cli', () => {
25732576
expect(httpSource).toContain('allowedOrigins: [process.env.SITE_URL!]');
25742577
expect(httpSource).not.toContain('authMiddleware');
25752578
expect(httpSource).not.toContain('createHttpRouter');
2579+
expectDependencyInstallCallWithPackages(
2580+
execaStub.mock.calls as unknown as unknown[],
2581+
[OPENTELEMETRY_API_INSTALL_SPEC, resolveScaffoldInstallSpec()]
2582+
);
25762583

25772584
const schemaSource = fs.readFileSync(
25782585
path.join(dir, 'convex', 'schema.ts'),

packages/kitcn/src/cli/registry/dependencies.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,20 @@ export const applyDependencyHintsInstall = async (
172172
const missingDependencyHints = resolveMissingDependencyHints(
173173
dependencyHints
174174
).filter((dependencyHint) => !preinstalledSpecs.includes(dependencyHint));
175-
if (missingDependencyHints.length === 0) {
175+
const installSpecs = missingDependencyHints.map((dependencyHint) =>
176+
resolveSupportedDependencyInstallSpec(dependencyHint)
177+
);
178+
if (installSpecs.length === 0) {
176179
return preinstalledSpecs;
177180
}
178181

179182
const { packageJsonPath } = resolvePackageJsonInstallTarget();
180-
await execaFn('bun', ['add', ...missingDependencyHints], {
183+
await execaFn('bun', ['add', ...installSpecs], {
181184
cwd: dirname(packageJsonPath),
182185
stdio: 'inherit',
183186
});
184187

185-
return [...preinstalledSpecs, ...missingDependencyHints];
188+
return [...preinstalledSpecs, ...installSpecs];
186189
};
187190

188191
export const applyPlanningDependencyInstall = async (
@@ -193,17 +196,20 @@ export const applyPlanningDependencyInstall = async (
193196
const missingDependencySpecs = resolveMissingDependencyHints(
194197
dependencySpecs
195198
).filter((dependencySpec) => !preinstalledSpecs.includes(dependencySpec));
196-
if (missingDependencySpecs.length === 0) {
199+
const installSpecs = missingDependencySpecs.map((dependencySpec) =>
200+
resolveSupportedDependencyInstallSpec(dependencySpec)
201+
);
202+
if (installSpecs.length === 0) {
197203
return preinstalledSpecs;
198204
}
199205

200206
const { packageJsonPath } = resolvePackageJsonInstallTarget();
201-
await execaFn('bun', ['add', ...missingDependencySpecs], {
207+
await execaFn('bun', ['add', ...installSpecs], {
202208
cwd: dirname(packageJsonPath),
203209
stdio: 'inherit',
204210
});
205211

206-
return [...preinstalledSpecs, ...missingDependencySpecs];
212+
return [...preinstalledSpecs, ...installSpecs];
207213
};
208214

209215
export const applyPluginDependencyInstall = async (

packages/kitcn/src/cli/registry/items/auth/auth-item.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ const AUTH_CONVEX_FILES = [
138138
target: 'functions',
139139
content: AUTH_CONVEX_TEMPLATE,
140140
requires: ['auth-config-convex'],
141-
dependencyHintMessage: 'Auth runtime depends on OpenTelemetry API.',
142-
dependencyHints: [OPENTELEMETRY_API_INSTALL_SPEC],
141+
dependencyHintMessage:
142+
'Auth runtime depends on OpenTelemetry API and kitcn runtime helpers.',
143+
dependencyHints: [OPENTELEMETRY_API_INSTALL_SPEC, 'kitcn'],
143144
}),
144145
createRegistryFile({
145146
id: 'auth-client-convex',

packages/kitcn/src/cli/supported-dependencies.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, test } from 'bun:test';
2+
import fs from 'node:fs';
23
import {
34
BASELINE_DEPENDENCY_INSTALL_SPECS,
45
BETTER_AUTH_INSTALL_SPEC,
@@ -9,6 +10,7 @@ import {
910
PINNED_HONO_INSTALL_SPEC,
1011
PINNED_TANSTACK_REACT_QUERY_INSTALL_SPEC,
1112
PINNED_ZOD_INSTALL_SPEC,
13+
resolveScaffoldInstallSpec,
1214
resolveSupportedDependencyInstallSpec,
1315
SUPPORTED_DEPENDENCY_VERSIONS,
1416
} from './supported-dependencies';
@@ -73,4 +75,15 @@ describe('cli/supported-dependencies', () => {
7375
resolveSupportedDependencyInstallSpec('better-auth@1.5.3', env)
7476
).toBe('better-auth@1.5.3');
7577
});
78+
79+
test('pins scaffold kitcn installs to the current package version', () => {
80+
const packageJson = JSON.parse(
81+
fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
82+
) as { version: string };
83+
84+
expect(resolveScaffoldInstallSpec({})).toBe(`kitcn@${packageJson.version}`);
85+
expect(resolveSupportedDependencyInstallSpec('kitcn', {})).toBe(
86+
`kitcn@${packageJson.version}`
87+
);
88+
});
7689
});

packages/kitcn/src/cli/supported-dependencies.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import fs from 'node:fs';
2+
import { dirname, join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
15
const EXACT_VERSION_RE = /^(\d+)\.(\d+)\.\d+$/;
26
const SUPPORTED_CONVEX_VERSION = '1.33.0';
37
const SUPPORTED_BETTER_AUTH_VERSION = '1.5.3';
@@ -9,6 +13,8 @@ const SUPPORTED_ZOD_VERSION = '4.3.6';
913
export const KITCN_INSTALL_SPEC_ENV = 'KITCN_INSTALL_SPEC';
1014
export const KITCN_RESEND_INSTALL_SPEC_ENV = 'KITCN_RESEND_INSTALL_SPEC';
1115

16+
let ownVersion: string | null | undefined;
17+
1218
export function getMinimumVersionRange(version: string): string {
1319
const match = EXACT_VERSION_RE.exec(version);
1420
if (!match) {
@@ -55,6 +61,10 @@ export function resolveSupportedDependencyInstallSpec(
5561
spec: string,
5662
env: Record<string, string | undefined> = process.env
5763
) {
64+
if (getPackageNameFromInstallSpec(spec) === 'kitcn') {
65+
return resolveScaffoldInstallSpec(env);
66+
}
67+
5868
const envKey =
5969
LOCAL_INSTALL_SPEC_ENV_BY_PACKAGE_NAME[
6070
getPackageNameFromInstallSpec(
@@ -65,6 +75,46 @@ export function resolveSupportedDependencyInstallSpec(
6575
return override && override.length > 0 ? override : spec;
6676
}
6777

78+
function readOwnVersion() {
79+
if (ownVersion !== undefined) {
80+
return ownVersion ?? undefined;
81+
}
82+
83+
let currentDir = dirname(fileURLToPath(import.meta.url));
84+
while (true) {
85+
const packageJsonPath = join(currentDir, 'package.json');
86+
if (fs.existsSync(packageJsonPath)) {
87+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as {
88+
name?: string;
89+
version?: string;
90+
};
91+
if (parsed.name === 'kitcn') {
92+
ownVersion = parsed.version ?? null;
93+
return ownVersion ?? undefined;
94+
}
95+
}
96+
97+
const parentDir = dirname(currentDir);
98+
if (parentDir === currentDir) {
99+
ownVersion = null;
100+
return undefined;
101+
}
102+
currentDir = parentDir;
103+
}
104+
}
105+
106+
export function resolveScaffoldInstallSpec(
107+
env: Record<string, string | undefined> = process.env
108+
) {
109+
const override = env[KITCN_INSTALL_SPEC_ENV]?.trim();
110+
if (override) {
111+
return override;
112+
}
113+
114+
const version = readOwnVersion();
115+
return version ? `kitcn@${version}` : 'kitcn';
116+
}
117+
68118
export const SUPPORTED_DEPENDENCY_VERSIONS = {
69119
convex: {
70120
exact: SUPPORTED_CONVEX_VERSION,

0 commit comments

Comments
 (0)