Skip to content

Commit fbd0ed5

Browse files
committed
security fixes in convex, and prepped to ship issue fixes
1 parent c3b168f commit fbd0ed5

File tree

26 files changed

+1738
-330
lines changed

26 files changed

+1738
-330
lines changed

ISSUE_IMPLEMENTATION_PLAN.md

Lines changed: 1072 additions & 0 deletions
Large diffs are not rendered by default.

apps/cli/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "btca",
33
"author": "Ben Davis",
4-
"version": "1.0.51",
4+
"version": "1.0.52",
55
"homepage": "https://btca.dev",
66
"description": "CLI tool for asking questions about technologies using btca server",
77
"type": "module",
@@ -41,8 +41,8 @@
4141
"devDependencies": {
4242
"@btca/shared": "workspace:*",
4343
"btca-server": "workspace:*",
44-
"@opentui/core": "^0.1.65",
45-
"@opentui/solid": "^0.1.65",
44+
"@opentui/core": "0.1.65",
45+
"@opentui/solid": "0.1.65",
4646
"@shikijs/langs": "^3.20.0",
4747
"@shikijs/themes": "^3.20.0",
4848
"@types/bun": "latest",

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "btca-server",
3-
"version": "1.0.50",
3+
"version": "1.0.52",
44
"description": "BTCA server for answering questions about your codebase using OpenCode AI",
55
"author": "Ben Davis",
66
"license": "MIT",

apps/web/src/convex/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type * as analytics from "../analytics.js";
1212
import type * as analyticsEvents from "../analyticsEvents.js";
1313
import type * as apiHelpers from "../apiHelpers.js";
1414
import type * as apiKeys from "../apiKeys.js";
15+
import type * as authHelpers from "../authHelpers.js";
1516
import type * as crons from "../crons.js";
1617
import type * as http from "../http.js";
1718
import type * as instances_actions from "../instances/actions.js";
@@ -41,6 +42,7 @@ declare const fullApi: ApiFromModules<{
4142
analyticsEvents: typeof analyticsEvents;
4243
apiHelpers: typeof apiHelpers;
4344
apiKeys: typeof apiKeys;
45+
authHelpers: typeof authHelpers;
4446
crons: typeof crons;
4547
http: typeof http;
4648
"instances/actions": typeof instances_actions;

apps/web/src/convex/apiHelpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ type NestedApi = typeof api & {
2323
type NestedInternal = typeof internal & {
2424
'scheduled/queries': InternalQueryRecord;
2525
'instances/actions': InternalActionRecord;
26+
'instances/queries': InternalQueryRecord;
2627
};
2728

2829
type InstancesApi = {
2930
queries: PublicQueryRecord;
3031
mutations: PublicMutationRecord;
3132
actions: PublicActionRecord;
33+
internalQueries: InternalQueryRecord;
3234
internalActions: InternalActionRecord;
3335
};
3436

@@ -40,6 +42,7 @@ export const instances: InstancesApi = {
4042
queries: (api as NestedApi)['instances/queries'],
4143
mutations: (api as NestedApi)['instances/mutations'],
4244
actions: (api as NestedApi)['instances/actions'],
45+
internalQueries: (internal as NestedInternal)['instances/queries'],
4346
internalActions: (internal as NestedInternal)['instances/actions']
4447
};
4548

apps/web/src/convex/apiKeys.ts

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ import { v } from 'convex/values';
33

44
import { internal } from './_generated/api';
55
import { AnalyticsEvents } from './analyticsEvents';
6+
import { getAuthenticatedInstance, requireApiKeyOwnership } from './authHelpers';
7+
8+
/**
9+
* List API keys for the authenticated user's instance
10+
*/
11+
export const list = query({
12+
args: {},
13+
handler: async (ctx) => {
14+
const instance = await getAuthenticatedInstance(ctx);
615

7-
export const listByUser = query({
8-
args: { userId: v.id('instances') },
9-
handler: async (ctx, args) => {
1016
const keys = await ctx.db
1117
.query('apiKeys')
12-
.withIndex('by_instance', (q) => q.eq('instanceId', args.userId))
18+
.withIndex('by_instance', (q) => q.eq('instanceId', instance._id))
1319
.collect();
1420

1521
return keys.map((k) => ({
@@ -24,37 +30,37 @@ export const listByUser = query({
2430
}
2531
});
2632

33+
/**
34+
* Create an API key for the authenticated user's instance
35+
*/
2736
export const create = mutation({
2837
args: {
29-
userId: v.id('instances'),
3038
name: v.string()
3139
},
3240
handler: async (ctx, args) => {
33-
const instance = await ctx.db.get(args.userId);
41+
const instance = await getAuthenticatedInstance(ctx);
3442

3543
const key = generateApiKey();
3644
const keyHash = await hashApiKey(key);
3745
const keyPrefix = key.slice(0, 8);
3846

3947
const id = await ctx.db.insert('apiKeys', {
40-
instanceId: args.userId,
48+
instanceId: instance._id,
4149
name: args.name,
4250
keyHash,
4351
keyPrefix,
4452
createdAt: Date.now()
4553
});
4654

47-
if (instance) {
48-
await ctx.scheduler.runAfter(0, internal.analytics.trackEvent, {
49-
distinctId: instance.clerkId,
50-
event: AnalyticsEvents.API_KEY_CREATED,
51-
properties: {
52-
instanceId: args.userId,
53-
keyId: id,
54-
keyName: args.name
55-
}
56-
});
57-
}
55+
await ctx.scheduler.runAfter(0, internal.analytics.trackEvent, {
56+
distinctId: instance.clerkId,
57+
event: AnalyticsEvents.API_KEY_CREATED,
58+
properties: {
59+
instanceId: instance._id,
60+
keyId: id,
61+
keyName: args.name
62+
}
63+
});
5864

5965
return { id, key };
6066
}
@@ -69,29 +75,32 @@ function generateApiKey(): string {
6975
return result;
7076
}
7177

78+
/**
79+
* Revoke an API key owned by the authenticated user
80+
*/
7281
export const revoke = mutation({
7382
args: { keyId: v.id('apiKeys') },
7483
handler: async (ctx, args) => {
75-
const apiKey = await ctx.db.get(args.keyId);
76-
const instance = apiKey ? await ctx.db.get(apiKey.instanceId) : null;
84+
const { apiKey, instance } = await requireApiKeyOwnership(ctx, args.keyId);
7785

7886
await ctx.db.patch(args.keyId, {
7987
revokedAt: Date.now()
8088
});
8189

82-
if (instance && apiKey) {
83-
await ctx.scheduler.runAfter(0, internal.analytics.trackEvent, {
84-
distinctId: instance.clerkId,
85-
event: AnalyticsEvents.API_KEY_REVOKED,
86-
properties: {
87-
instanceId: apiKey.instanceId,
88-
keyId: args.keyId
89-
}
90-
});
91-
}
90+
await ctx.scheduler.runAfter(0, internal.analytics.trackEvent, {
91+
distinctId: instance.clerkId,
92+
event: AnalyticsEvents.API_KEY_REVOKED,
93+
properties: {
94+
instanceId: apiKey.instanceId,
95+
keyId: args.keyId
96+
}
97+
});
9298
}
9399
});
94100

101+
/**
102+
* Validate an API key (internal use - no auth required as this validates the key itself)
103+
*/
95104
export const validate = query({
96105
args: { apiKey: v.string() },
97106
handler: async (ctx, args) => {
@@ -124,6 +133,9 @@ export const validate = query({
124133
}
125134
});
126135

136+
/**
137+
* Touch last used timestamp for an API key (internal use for tracking)
138+
*/
127139
export const touchLastUsed = mutation({
128140
args: { keyId: v.id('apiKeys') },
129141
handler: async (ctx, args) => {

0 commit comments

Comments
 (0)