@doccov/api 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/api/index.ts +12 -5
- package/migrations/001_initial.ts +151 -0
- package/migrations/002_polar_billing.ts +17 -0
- package/migrations/003_verification_table.ts +18 -0
- package/migrations/004_rename_tables_singular.ts +14 -0
- package/package.json +14 -3
- package/src/auth/config.ts +53 -0
- package/src/auth/hooks.ts +45 -0
- package/src/db/client.ts +15 -0
- package/src/db/migrate.ts +38 -0
- package/src/index.ts +35 -5
- package/src/middleware/api-key-auth.ts +94 -0
- package/src/middleware/org-rate-limit.ts +78 -0
- package/src/routes/api-keys.ts +127 -0
- package/src/routes/auth.ts +62 -0
- package/src/routes/billing.ts +202 -0
- package/src/routes/coverage.ts +270 -0
- package/src/routes/orgs.ts +138 -0
- package/src/routes/plan.ts +2 -72
- package/src/server.ts +22 -0
- package/src/utils/api-keys.ts +20 -0
- package/vercel.json +1 -3
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { auth } from '../auth/config';
|
|
4
|
+
import { db } from '../db/client';
|
|
5
|
+
|
|
6
|
+
type Session = Awaited<ReturnType<typeof auth.api.getSession>>;
|
|
7
|
+
|
|
8
|
+
type Env = {
|
|
9
|
+
Variables: {
|
|
10
|
+
session: NonNullable<Session>;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const coverageRoute = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
// Middleware: require auth
|
|
17
|
+
coverageRoute.use('*', async (c, next) => {
|
|
18
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
19
|
+
if (!session) {
|
|
20
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
21
|
+
}
|
|
22
|
+
c.set('session', session);
|
|
23
|
+
await next();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Get coverage history for a project
|
|
27
|
+
coverageRoute.get('/projects/:projectId/history', async (c) => {
|
|
28
|
+
const session = c.get('session');
|
|
29
|
+
const { projectId } = c.req.param();
|
|
30
|
+
const { range = '30d', limit = '50' } = c.req.query();
|
|
31
|
+
|
|
32
|
+
// Verify user has access to project
|
|
33
|
+
const project = await db
|
|
34
|
+
.selectFrom('projects')
|
|
35
|
+
.innerJoin('org_members', 'org_members.orgId', 'projects.orgId')
|
|
36
|
+
.where('projects.id', '=', projectId)
|
|
37
|
+
.where('org_members.userId', '=', session.user.id)
|
|
38
|
+
.select(['projects.id', 'projects.name'])
|
|
39
|
+
.executeTakeFirst();
|
|
40
|
+
|
|
41
|
+
if (!project) {
|
|
42
|
+
return c.json({ error: 'Project not found' }, 404);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Calculate date filter based on range
|
|
46
|
+
let dateFilter: Date | null = null;
|
|
47
|
+
const now = new Date();
|
|
48
|
+
switch (range) {
|
|
49
|
+
case '7d':
|
|
50
|
+
dateFilter = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
51
|
+
break;
|
|
52
|
+
case '30d':
|
|
53
|
+
dateFilter = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
54
|
+
break;
|
|
55
|
+
case '90d':
|
|
56
|
+
dateFilter = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
|
57
|
+
break;
|
|
58
|
+
// 'all' and 'versions' - no date filter
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let query = db
|
|
62
|
+
.selectFrom('coverage_snapshots')
|
|
63
|
+
.where('projectId', '=', projectId)
|
|
64
|
+
.orderBy('createdAt', 'desc')
|
|
65
|
+
.limit(parseInt(limit, 10));
|
|
66
|
+
|
|
67
|
+
if (dateFilter) {
|
|
68
|
+
query = query.where('createdAt', '>=', dateFilter);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const snapshots = await query
|
|
72
|
+
.select([
|
|
73
|
+
'id',
|
|
74
|
+
'version',
|
|
75
|
+
'branch',
|
|
76
|
+
'commitSha',
|
|
77
|
+
'coveragePercent',
|
|
78
|
+
'documentedCount',
|
|
79
|
+
'totalCount',
|
|
80
|
+
'descriptionCount',
|
|
81
|
+
'paramsCount',
|
|
82
|
+
'returnsCount',
|
|
83
|
+
'examplesCount',
|
|
84
|
+
'driftCount',
|
|
85
|
+
'source',
|
|
86
|
+
'createdAt',
|
|
87
|
+
])
|
|
88
|
+
.execute();
|
|
89
|
+
|
|
90
|
+
// Reverse to get chronological order
|
|
91
|
+
const chronological = snapshots.reverse();
|
|
92
|
+
|
|
93
|
+
// Calculate insights
|
|
94
|
+
const insights = generateInsights(chronological);
|
|
95
|
+
|
|
96
|
+
// Detect regressions
|
|
97
|
+
const regression = detectRegression(chronological);
|
|
98
|
+
|
|
99
|
+
return c.json({
|
|
100
|
+
snapshots: chronological,
|
|
101
|
+
insights,
|
|
102
|
+
regression,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Record a new coverage snapshot
|
|
107
|
+
coverageRoute.post('/projects/:projectId/snapshots', async (c) => {
|
|
108
|
+
const session = c.get('session');
|
|
109
|
+
const { projectId } = c.req.param();
|
|
110
|
+
const body = await c.req.json<{
|
|
111
|
+
version?: string;
|
|
112
|
+
branch?: string;
|
|
113
|
+
commitSha?: string;
|
|
114
|
+
coveragePercent: number;
|
|
115
|
+
documentedCount: number;
|
|
116
|
+
totalCount: number;
|
|
117
|
+
descriptionCount?: number;
|
|
118
|
+
paramsCount?: number;
|
|
119
|
+
returnsCount?: number;
|
|
120
|
+
examplesCount?: number;
|
|
121
|
+
driftCount?: number;
|
|
122
|
+
source?: 'ci' | 'manual' | 'scheduled';
|
|
123
|
+
}>();
|
|
124
|
+
|
|
125
|
+
// Verify user has admin access to project
|
|
126
|
+
const membership = await db
|
|
127
|
+
.selectFrom('projects')
|
|
128
|
+
.innerJoin('org_members', 'org_members.orgId', 'projects.orgId')
|
|
129
|
+
.where('projects.id', '=', projectId)
|
|
130
|
+
.where('org_members.userId', '=', session.user.id)
|
|
131
|
+
.where('org_members.role', 'in', ['owner', 'admin'])
|
|
132
|
+
.select(['projects.id'])
|
|
133
|
+
.executeTakeFirst();
|
|
134
|
+
|
|
135
|
+
if (!membership) {
|
|
136
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const snapshot = await db
|
|
140
|
+
.insertInto('coverage_snapshots')
|
|
141
|
+
.values({
|
|
142
|
+
id: nanoid(21),
|
|
143
|
+
projectId,
|
|
144
|
+
version: body.version || null,
|
|
145
|
+
branch: body.branch || null,
|
|
146
|
+
commitSha: body.commitSha || null,
|
|
147
|
+
coveragePercent: body.coveragePercent,
|
|
148
|
+
documentedCount: body.documentedCount,
|
|
149
|
+
totalCount: body.totalCount,
|
|
150
|
+
descriptionCount: body.descriptionCount || null,
|
|
151
|
+
paramsCount: body.paramsCount || null,
|
|
152
|
+
returnsCount: body.returnsCount || null,
|
|
153
|
+
examplesCount: body.examplesCount || null,
|
|
154
|
+
driftCount: body.driftCount || 0,
|
|
155
|
+
source: body.source || 'manual',
|
|
156
|
+
})
|
|
157
|
+
.returningAll()
|
|
158
|
+
.executeTakeFirst();
|
|
159
|
+
|
|
160
|
+
// Update project's latest coverage
|
|
161
|
+
await db
|
|
162
|
+
.updateTable('projects')
|
|
163
|
+
.set({
|
|
164
|
+
coverageScore: body.coveragePercent,
|
|
165
|
+
driftCount: body.driftCount || 0,
|
|
166
|
+
lastAnalyzedAt: new Date(),
|
|
167
|
+
})
|
|
168
|
+
.where('id', '=', projectId)
|
|
169
|
+
.execute();
|
|
170
|
+
|
|
171
|
+
return c.json({ snapshot }, 201);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Helper: Generate insights from coverage data
|
|
175
|
+
interface Snapshot {
|
|
176
|
+
version: string | null;
|
|
177
|
+
coveragePercent: number;
|
|
178
|
+
documentedCount: number;
|
|
179
|
+
totalCount: number;
|
|
180
|
+
driftCount: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface Insight {
|
|
184
|
+
type: 'improvement' | 'regression' | 'prediction' | 'milestone';
|
|
185
|
+
message: string;
|
|
186
|
+
severity: 'info' | 'warning' | 'success';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function generateInsights(snapshots: Snapshot[]): Insight[] {
|
|
190
|
+
const insights: Insight[] = [];
|
|
191
|
+
if (snapshots.length < 2) return insights;
|
|
192
|
+
|
|
193
|
+
const first = snapshots[0];
|
|
194
|
+
const last = snapshots[snapshots.length - 1];
|
|
195
|
+
const diff = last.coveragePercent - first.coveragePercent;
|
|
196
|
+
|
|
197
|
+
// Overall improvement/regression
|
|
198
|
+
if (diff > 0) {
|
|
199
|
+
insights.push({
|
|
200
|
+
type: 'improvement',
|
|
201
|
+
message: `Coverage increased ${diff.toFixed(0)}% since ${first.version || 'first snapshot'}`,
|
|
202
|
+
severity: 'success',
|
|
203
|
+
});
|
|
204
|
+
} else if (diff < 0) {
|
|
205
|
+
insights.push({
|
|
206
|
+
type: 'regression',
|
|
207
|
+
message: `Coverage decreased ${Math.abs(diff).toFixed(0)}% since ${first.version || 'first snapshot'}`,
|
|
208
|
+
severity: 'warning',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Predict time to 100%
|
|
213
|
+
if (diff > 0 && last.coveragePercent < 100) {
|
|
214
|
+
const remaining = 100 - last.coveragePercent;
|
|
215
|
+
const avgGainPerSnapshot = diff / (snapshots.length - 1);
|
|
216
|
+
if (avgGainPerSnapshot > 0) {
|
|
217
|
+
const snapshotsToComplete = Math.ceil(remaining / avgGainPerSnapshot);
|
|
218
|
+
insights.push({
|
|
219
|
+
type: 'prediction',
|
|
220
|
+
message: `At current pace, 100% coverage in ~${snapshotsToComplete} releases`,
|
|
221
|
+
severity: 'info',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for milestones
|
|
227
|
+
const milestones = [50, 75, 90, 100];
|
|
228
|
+
for (const milestone of milestones) {
|
|
229
|
+
const crossedAt = snapshots.findIndex(
|
|
230
|
+
(s, i) =>
|
|
231
|
+
i > 0 && s.coveragePercent >= milestone && snapshots[i - 1].coveragePercent < milestone,
|
|
232
|
+
);
|
|
233
|
+
if (crossedAt > 0) {
|
|
234
|
+
insights.push({
|
|
235
|
+
type: 'milestone',
|
|
236
|
+
message: `Reached ${milestone}% coverage at ${snapshots[crossedAt].version || `snapshot ${crossedAt + 1}`}`,
|
|
237
|
+
severity: 'success',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return insights.slice(0, 5); // Limit to 5 insights
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Helper: Detect recent regression
|
|
246
|
+
function detectRegression(
|
|
247
|
+
snapshots: Snapshot[],
|
|
248
|
+
): { fromVersion: string; toVersion: string; coverageDrop: number; exportsLost: number } | null {
|
|
249
|
+
if (snapshots.length < 2) return null;
|
|
250
|
+
|
|
251
|
+
// Look at last 5 snapshots for recent regressions
|
|
252
|
+
const recent = snapshots.slice(-5);
|
|
253
|
+
for (let i = 1; i < recent.length; i++) {
|
|
254
|
+
const prev = recent[i - 1];
|
|
255
|
+
const curr = recent[i];
|
|
256
|
+
const drop = prev.coveragePercent - curr.coveragePercent;
|
|
257
|
+
|
|
258
|
+
if (drop >= 3) {
|
|
259
|
+
// 3% or more drop
|
|
260
|
+
return {
|
|
261
|
+
fromVersion: prev.version || `v${i}`,
|
|
262
|
+
toVersion: curr.version || `v${i + 1}`,
|
|
263
|
+
coverageDrop: Math.round(drop),
|
|
264
|
+
exportsLost: prev.documentedCount - curr.documentedCount,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { auth } from '../auth/config';
|
|
4
|
+
import { db } from '../db/client';
|
|
5
|
+
|
|
6
|
+
type Session = Awaited<ReturnType<typeof auth.api.getSession>>;
|
|
7
|
+
|
|
8
|
+
type Env = {
|
|
9
|
+
Variables: {
|
|
10
|
+
session: NonNullable<Session>;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const orgsRoute = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
// Middleware: require auth
|
|
17
|
+
orgsRoute.use('*', async (c, next) => {
|
|
18
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
19
|
+
if (!session) {
|
|
20
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
21
|
+
}
|
|
22
|
+
c.set('session', session);
|
|
23
|
+
await next();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// List user's organizations
|
|
27
|
+
orgsRoute.get('/', async (c) => {
|
|
28
|
+
const session = c.get('session');
|
|
29
|
+
|
|
30
|
+
const memberships = await db
|
|
31
|
+
.selectFrom('org_members')
|
|
32
|
+
.innerJoin('organizations', 'organizations.id', 'org_members.orgId')
|
|
33
|
+
.where('org_members.userId', '=', session.user.id)
|
|
34
|
+
.select([
|
|
35
|
+
'organizations.id',
|
|
36
|
+
'organizations.name',
|
|
37
|
+
'organizations.slug',
|
|
38
|
+
'organizations.plan',
|
|
39
|
+
'organizations.isPersonal',
|
|
40
|
+
'organizations.aiCallsUsed',
|
|
41
|
+
'org_members.role',
|
|
42
|
+
])
|
|
43
|
+
.execute();
|
|
44
|
+
|
|
45
|
+
return c.json({ organizations: memberships });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Get single org by slug
|
|
49
|
+
orgsRoute.get('/:slug', async (c) => {
|
|
50
|
+
const session = c.get('session');
|
|
51
|
+
const { slug } = c.req.param();
|
|
52
|
+
|
|
53
|
+
const org = await db
|
|
54
|
+
.selectFrom('organizations')
|
|
55
|
+
.innerJoin('org_members', 'org_members.orgId', 'organizations.id')
|
|
56
|
+
.where('organizations.slug', '=', slug)
|
|
57
|
+
.where('org_members.userId', '=', session.user.id)
|
|
58
|
+
.select([
|
|
59
|
+
'organizations.id',
|
|
60
|
+
'organizations.name',
|
|
61
|
+
'organizations.slug',
|
|
62
|
+
'organizations.plan',
|
|
63
|
+
'organizations.isPersonal',
|
|
64
|
+
'organizations.aiCallsUsed',
|
|
65
|
+
'organizations.aiCallsResetAt',
|
|
66
|
+
'org_members.role',
|
|
67
|
+
])
|
|
68
|
+
.executeTakeFirst();
|
|
69
|
+
|
|
70
|
+
if (!org) {
|
|
71
|
+
return c.json({ error: 'Organization not found' }, 404);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return c.json({ organization: org });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Get org's projects
|
|
78
|
+
orgsRoute.get('/:slug/projects', async (c) => {
|
|
79
|
+
const session = c.get('session');
|
|
80
|
+
const { slug } = c.req.param();
|
|
81
|
+
|
|
82
|
+
// Verify membership
|
|
83
|
+
const membership = await db
|
|
84
|
+
.selectFrom('org_members')
|
|
85
|
+
.innerJoin('organizations', 'organizations.id', 'org_members.orgId')
|
|
86
|
+
.where('organizations.slug', '=', slug)
|
|
87
|
+
.where('org_members.userId', '=', session.user.id)
|
|
88
|
+
.select(['org_members.orgId'])
|
|
89
|
+
.executeTakeFirst();
|
|
90
|
+
|
|
91
|
+
if (!membership) {
|
|
92
|
+
return c.json({ error: 'Organization not found' }, 404);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const projects = await db
|
|
96
|
+
.selectFrom('projects')
|
|
97
|
+
.where('orgId', '=', membership.orgId)
|
|
98
|
+
.selectAll()
|
|
99
|
+
.execute();
|
|
100
|
+
|
|
101
|
+
return c.json({ projects });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Create a project
|
|
105
|
+
orgsRoute.post('/:slug/projects', async (c) => {
|
|
106
|
+
const session = c.get('session');
|
|
107
|
+
const { slug } = c.req.param();
|
|
108
|
+
const body = await c.req.json<{ name: string; fullName: string; isPrivate?: boolean }>();
|
|
109
|
+
|
|
110
|
+
// Verify owner/admin membership
|
|
111
|
+
const membership = await db
|
|
112
|
+
.selectFrom('org_members')
|
|
113
|
+
.innerJoin('organizations', 'organizations.id', 'org_members.orgId')
|
|
114
|
+
.where('organizations.slug', '=', slug)
|
|
115
|
+
.where('org_members.userId', '=', session.user.id)
|
|
116
|
+
.where('org_members.role', 'in', ['owner', 'admin'])
|
|
117
|
+
.select(['org_members.orgId'])
|
|
118
|
+
.executeTakeFirst();
|
|
119
|
+
|
|
120
|
+
if (!membership) {
|
|
121
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const project = await db
|
|
125
|
+
.insertInto('projects')
|
|
126
|
+
.values({
|
|
127
|
+
id: nanoid(21),
|
|
128
|
+
orgId: membership.orgId,
|
|
129
|
+
name: body.name,
|
|
130
|
+
fullName: body.fullName,
|
|
131
|
+
isPrivate: body.isPrivate ?? false,
|
|
132
|
+
defaultBranch: 'main',
|
|
133
|
+
})
|
|
134
|
+
.returningAll()
|
|
135
|
+
.executeTakeFirst();
|
|
136
|
+
|
|
137
|
+
return c.json({ project }, 201);
|
|
138
|
+
});
|
package/src/routes/plan.ts
CHANGED
|
@@ -1,81 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan route
|
|
3
|
-
* Does NOT use Vercel Sandbox - only GitHub API + Anthropic Claude
|
|
2
|
+
* Plan route - TODO: implement plan-agent
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
import { fetchGitHubContext, parseScanGitHubUrl } from '@doccov/sdk';
|
|
7
5
|
import { Hono } from 'hono';
|
|
8
|
-
import { generateBuildPlan } from '../../lib/plan-agent';
|
|
9
6
|
|
|
10
7
|
export const planRoute = new Hono();
|
|
11
8
|
|
|
12
9
|
planRoute.post('/', async (c) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!body.url) {
|
|
16
|
-
return c.json({ error: 'url is required' }, 400);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Validate URL format
|
|
20
|
-
let repoUrl: string;
|
|
21
|
-
try {
|
|
22
|
-
const parsed = parseScanGitHubUrl(body.url);
|
|
23
|
-
if (!parsed) {
|
|
24
|
-
return c.json({ error: 'Invalid GitHub URL' }, 400);
|
|
25
|
-
}
|
|
26
|
-
repoUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
|
|
27
|
-
} catch {
|
|
28
|
-
return c.json({ error: 'Invalid GitHub URL' }, 400);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
// Fetch project context from GitHub
|
|
33
|
-
const context = await fetchGitHubContext(repoUrl, body.ref);
|
|
34
|
-
|
|
35
|
-
// Check for private repos
|
|
36
|
-
if (context.metadata.isPrivate) {
|
|
37
|
-
return c.json(
|
|
38
|
-
{
|
|
39
|
-
error: 'Private repositories are not supported',
|
|
40
|
-
hint: 'Use a public repository or run doccov locally',
|
|
41
|
-
},
|
|
42
|
-
403,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Generate build plan using AI
|
|
47
|
-
const plan = await generateBuildPlan(context, {
|
|
48
|
-
targetPackage: body.package,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return c.json({
|
|
52
|
-
plan,
|
|
53
|
-
context: {
|
|
54
|
-
owner: context.metadata.owner,
|
|
55
|
-
repo: context.metadata.repo,
|
|
56
|
-
ref: context.ref,
|
|
57
|
-
packageManager: context.packageManager,
|
|
58
|
-
isMonorepo: context.workspace.isMonorepo,
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('Plan generation error:', error);
|
|
63
|
-
|
|
64
|
-
if (error instanceof Error) {
|
|
65
|
-
if (error.message.includes('404') || error.message.includes('not found')) {
|
|
66
|
-
return c.json({ error: 'Repository not found' }, 404);
|
|
67
|
-
}
|
|
68
|
-
if (error.message.includes('rate limit')) {
|
|
69
|
-
return c.json({ error: 'GitHub API rate limit exceeded' }, 429);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return c.json(
|
|
74
|
-
{
|
|
75
|
-
error: 'Failed to generate build plan',
|
|
76
|
-
message: error instanceof Error ? error.message : 'Unknown error',
|
|
77
|
-
},
|
|
78
|
-
500,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
10
|
+
return c.json({ error: 'Plan endpoint not yet implemented' }, 501);
|
|
81
11
|
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { runMigrations } from './db/migrate';
|
|
3
|
+
import app from './index';
|
|
4
|
+
|
|
5
|
+
const port = parseInt(process.env.PORT || '3001');
|
|
6
|
+
|
|
7
|
+
async function start() {
|
|
8
|
+
// Run migrations on startup
|
|
9
|
+
console.log('Running migrations...');
|
|
10
|
+
await runMigrations();
|
|
11
|
+
|
|
12
|
+
// Start server
|
|
13
|
+
console.log(`Starting server on port ${port}`);
|
|
14
|
+
serve({
|
|
15
|
+
fetch: app.fetch,
|
|
16
|
+
port,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start().catch(console.error);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
|
|
4
|
+
const KEY_PREFIX = 'doccov_';
|
|
5
|
+
|
|
6
|
+
export function generateApiKey(): { key: string; hash: string; prefix: string } {
|
|
7
|
+
const random = nanoid(32);
|
|
8
|
+
const key = `${KEY_PREFIX}${random}`;
|
|
9
|
+
const hash = hashApiKey(key);
|
|
10
|
+
const prefix = key.slice(0, 12);
|
|
11
|
+
return { key, hash, prefix };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hashApiKey(key: string): string {
|
|
15
|
+
return createHash('sha256').update(key).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isValidKeyFormat(key: string): boolean {
|
|
19
|
+
return key.startsWith(KEY_PREFIX) && key.length === KEY_PREFIX.length + 32;
|
|
20
|
+
}
|