@codraoss/api 0.9.4

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/dist/index.js ADDED
@@ -0,0 +1,1165 @@
1
+ // src/router.ts
2
+ import { Hono as Hono9 } from "hono";
3
+
4
+ // src/middleware/auth.ts
5
+ import { createMiddleware } from "hono/factory";
6
+
7
+ // src/http.ts
8
+ function jsonError(message, status = 400) {
9
+ return Response.json({ error: message }, { status });
10
+ }
11
+ function wantsHtml(request) {
12
+ const accept = request.headers.get("accept") ?? "";
13
+ return accept.includes("text/html");
14
+ }
15
+
16
+ // src/sessions.ts
17
+ import { deleteCookie, getCookie, setCookie } from "hono/cookie";
18
+ var SESSION_COOKIE_NAME = "codra_session";
19
+ var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
20
+ async function createSession(c, session) {
21
+ const token = await c.env.deps.sessionStore.createSession(session);
22
+ setCookie(c, SESSION_COOKIE_NAME, token, {
23
+ httpOnly: true,
24
+ sameSite: "Lax",
25
+ secure: true,
26
+ path: "/",
27
+ maxAge: SESSION_TTL_SECONDS
28
+ });
29
+ c.set("sessionToken", token);
30
+ c.set("sessionUser", session);
31
+ return token;
32
+ }
33
+ async function destroySession(c) {
34
+ const token = getCookie(c, SESSION_COOKIE_NAME);
35
+ if (token) {
36
+ await c.env.deps.sessionStore.destroySession(token);
37
+ }
38
+ c.set("sessionToken", null);
39
+ c.set("sessionUser", null);
40
+ deleteCookie(c, SESSION_COOKIE_NAME, {
41
+ path: "/"
42
+ });
43
+ }
44
+ async function readSession(c) {
45
+ const token = getCookie(c, SESSION_COOKIE_NAME) ?? null;
46
+ c.set("sessionToken", token);
47
+ if (!token) {
48
+ c.set("sessionUser", null);
49
+ return null;
50
+ }
51
+ const session = await c.env.deps.sessionStore.readSession(token);
52
+ c.set("sessionUser", session);
53
+ return session;
54
+ }
55
+
56
+ // src/middleware/auth.ts
57
+ var requireSession = createMiddleware(async (c, next) => {
58
+ const session = await readSession(c);
59
+ if (!session) {
60
+ if (wantsHtml(c.req.raw)) {
61
+ return c.redirect("/login");
62
+ }
63
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
64
+ }
65
+ await next();
66
+ });
67
+
68
+ // src/middleware/csrf.ts
69
+ import { createMiddleware as createMiddleware2 } from "hono/factory";
70
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
71
+ var REQUESTED_WITH = "XMLHttpRequest";
72
+ var requireCsrfHeader = createMiddleware2(async (c, next) => {
73
+ if (SAFE_METHODS.has(c.req.method.toUpperCase())) {
74
+ await next();
75
+ return;
76
+ }
77
+ if (c.req.header("x-requested-with") !== REQUESTED_WITH) {
78
+ return c.json({ error: "Forbidden" }, 403);
79
+ }
80
+ await next();
81
+ });
82
+
83
+ // src/logger.ts
84
+ import { AsyncLocalStorage } from "async_hooks";
85
+ import { formatLogRecord, setLoggerSink } from "@codraoss/core/logger";
86
+ var storage = new AsyncLocalStorage();
87
+ var Logger = class _Logger {
88
+ constructor(context = {}) {
89
+ this.context = context;
90
+ }
91
+ context;
92
+ withContext(newContext) {
93
+ return new _Logger({ ...this.context, ...newContext });
94
+ }
95
+ log(level, message, data) {
96
+ const store = storage.getStore() || {};
97
+ const output = formatLogRecord(level, message, [store, this.context], data);
98
+ if (level === "error") {
99
+ console.error(JSON.stringify(output));
100
+ } else if (level === "warn") {
101
+ console.warn(JSON.stringify(output));
102
+ } else {
103
+ console.log(JSON.stringify(output));
104
+ }
105
+ }
106
+ runWithContext(context, fn) {
107
+ return storage.run({ ...storage.getStore(), ...context }, fn);
108
+ }
109
+ info(message, data) {
110
+ this.log("info", message, data);
111
+ }
112
+ error(message, data) {
113
+ if (data instanceof Error) {
114
+ this.log("error", message, {
115
+ name: data.name,
116
+ message: data.message,
117
+ stack: data.stack
118
+ });
119
+ } else {
120
+ this.log("error", message, data);
121
+ }
122
+ }
123
+ warn(message, data) {
124
+ this.log("warn", message, data);
125
+ }
126
+ debug(message, data) {
127
+ this.log("debug", message, data);
128
+ }
129
+ };
130
+ var logger = new Logger();
131
+ setLoggerSink(logger);
132
+
133
+ // src/middleware/observability.ts
134
+ var observability = async (c, next) => {
135
+ const requestId = c.req.header("x-request-id") || crypto.randomUUID();
136
+ c.set("requestId", requestId);
137
+ return logger.runWithContext({
138
+ requestId,
139
+ method: c.req.method,
140
+ path: c.req.path
141
+ }, async () => {
142
+ logger.info(`Incoming request: ${c.req.method} ${c.req.path}`);
143
+ const start = Date.now();
144
+ await next();
145
+ const duration = Date.now() - start;
146
+ logger.info(`Request completed: ${c.req.method} ${c.req.path}`, {
147
+ status: c.res.status,
148
+ durationMs: duration
149
+ });
150
+ });
151
+ };
152
+
153
+ // src/routes/auth.ts
154
+ import { Hono } from "hono";
155
+ function redirectToLogin(reason) {
156
+ const params = new URLSearchParams({ error: reason });
157
+ return `/login?${params.toString()}`;
158
+ }
159
+ function parseAllowedUsers(input) {
160
+ return new Set(
161
+ input.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
162
+ );
163
+ }
164
+ function createAuthRouter() {
165
+ const app = new Hono();
166
+ app.get("/github", async (c) => {
167
+ const state = await c.env.deps.authProvider.createOAuthState();
168
+ const result = await c.env.deps.authProvider.beginAuthorization(c.env.AUTH_CALLBACK_URL, state);
169
+ return c.redirect(result.url, 302);
170
+ });
171
+ app.get("/github/callback", async (c) => {
172
+ const error = c.req.query("error");
173
+ if (error) {
174
+ return c.redirect(redirectToLogin(error), 302);
175
+ }
176
+ const code = c.req.query("code")?.trim();
177
+ const state = c.req.query("state")?.trim();
178
+ if (!code || !state) {
179
+ return c.redirect(redirectToLogin("invalid_callback"), 302);
180
+ }
181
+ const stateMatches = await c.env.deps.authProvider.consumeOAuthState(state);
182
+ if (!stateMatches) {
183
+ return c.redirect(redirectToLogin("invalid_state"), 302);
184
+ }
185
+ try {
186
+ const { identity } = await c.env.deps.authProvider.completeAuthorization(code, state, state);
187
+ const allowedUsers = parseAllowedUsers(c.env.DASHBOARD_ALLOWED_USERS);
188
+ if (!allowedUsers.has(identity.login.toLowerCase())) {
189
+ return c.redirect(redirectToLogin("not_allowed"), 302);
190
+ }
191
+ await destroySession(c);
192
+ await createSession(c, identity);
193
+ try {
194
+ await c.env.deps.repositories.accounts.upsertAccountSettings(c.env, {
195
+ githubUserId: Number(identity.providerUserId),
196
+ githubUsername: identity.login,
197
+ accountName: identity.name,
198
+ accountEmail: identity.email
199
+ });
200
+ } catch (err) {
201
+ c.env.deps.platform.logger.warn("Failed to persist account settings on sign-in", {
202
+ error: err instanceof Error ? err.message : String(err)
203
+ });
204
+ }
205
+ return c.redirect("/dashboard", 302);
206
+ } catch {
207
+ return c.redirect(redirectToLogin("oauth_failed"), 302);
208
+ }
209
+ });
210
+ app.post("/logout", async (c) => {
211
+ await destroySession(c);
212
+ return c.json({ ok: true });
213
+ });
214
+ return app;
215
+ }
216
+
217
+ // src/routes/webhook.ts
218
+ import { Hono as Hono2 } from "hono";
219
+ import {
220
+ isFeedbackGitHubWebhookEvent
221
+ } from "@codraoss/schema/github";
222
+ async function handleFeedbackEvent(c, input) {
223
+ const { eventName, payload, repositoryId } = input;
224
+ if (repositoryId === null) return 0;
225
+ const botLogin = (c.env.BOT_USERNAME ?? "").toLowerCase();
226
+ const isOurs = (comment) => Boolean(botLogin) && (comment.user?.login ?? "").toLowerCase().startsWith(botLogin);
227
+ const prNumber = payload.pull_request?.number ?? null;
228
+ const entries = [];
229
+ const reopened = [];
230
+ const add = (comment, outcome) => {
231
+ if (!isOurs(comment) || !comment?.body) return;
232
+ const markerMatch = (comment.body || "").match(/<!-- codra-fp: (.*?) -->/);
233
+ if (!markerMatch) return;
234
+ let fingerprint = markerMatch[1];
235
+ let anchorHash = "";
236
+ let fingerprintV2 = null;
237
+ try {
238
+ const parts = JSON.parse(fingerprint);
239
+ if (Array.isArray(parts) && parts.length >= 2) {
240
+ fingerprintV2 = parts[0];
241
+ fingerprint = parts[1];
242
+ if (parts.length >= 3) anchorHash = parts[2];
243
+ }
244
+ } catch {
245
+ }
246
+ entries.push({
247
+ repositoryId,
248
+ prNumber,
249
+ fingerprint,
250
+ anchorHash,
251
+ fingerprintV2,
252
+ githubCommentId: comment.id,
253
+ outcome
254
+ });
255
+ };
256
+ if (eventName === "pull_request_review_comment") {
257
+ const { action, comment } = payload;
258
+ if (action === "created") add(comment, "posted");
259
+ else if (action === "deleted") add(comment, "deleted");
260
+ } else if (eventName === "pull_request_review_thread") {
261
+ const { action, thread } = payload;
262
+ for (const comment of thread?.comments ?? []) {
263
+ if (action === "resolved") {
264
+ add(comment, "resolved");
265
+ } else if (action === "unresolved") {
266
+ add(comment, "unresolved");
267
+ if (isOurs(comment)) reopened.push(comment.id);
268
+ }
269
+ }
270
+ }
271
+ if (entries.length === 0) return 0;
272
+ try {
273
+ const feedbackRepo = c.env.deps.repositories.commentFeedback;
274
+ if (reopened.length > 0) await feedbackRepo.clearResolvedFeedback(c.env, repositoryId, reopened);
275
+ return await feedbackRepo.recordCommentFeedback(c.env, entries);
276
+ } catch (error) {
277
+ c.env.deps.platform.logger.warn("Could not record comment feedback", {
278
+ eventName,
279
+ repositoryId,
280
+ error: error instanceof Error ? error.message : String(error)
281
+ });
282
+ return 0;
283
+ }
284
+ }
285
+ async function handleGitHubWebhook(c) {
286
+ const eventName = c.req.header("x-github-event");
287
+ const deliveryId = c.req.header("x-github-delivery");
288
+ const signature = c.req.header("x-hub-signature-256");
289
+ const rawBody = await c.req.text();
290
+ if (!eventName || !deliveryId) {
291
+ return jsonError("Missing GitHub webhook headers.", 400);
292
+ }
293
+ const verified = await c.env.deps.webhook.verifySignature(signature ?? null, rawBody);
294
+ if (!verified) {
295
+ return jsonError("Invalid webhook signature.", 401);
296
+ }
297
+ let payload;
298
+ try {
299
+ payload = JSON.parse(rawBody);
300
+ } catch {
301
+ return jsonError("Invalid webhook JSON payload.", 400);
302
+ }
303
+ const isFeedbackEvent = isFeedbackGitHubWebhookEvent(eventName);
304
+ const delivery = await c.env.deps.repositories.webhookDeliveries.recordWebhookDelivery(c.env, {
305
+ deliveryId,
306
+ eventName,
307
+ owner: "repository" in payload ? payload.repository.owner.login : null,
308
+ repo: "repository" in payload ? payload.repository.name : null,
309
+ payload: isFeedbackEvent ? null : payload
310
+ });
311
+ if (!delivery.inserted) {
312
+ return c.json({ ok: true, duplicate: true }, 202);
313
+ }
314
+ const installationId = String(payload.installation?.id ?? "");
315
+ if (!installationId || !("repository" in payload) || !payload.repository) {
316
+ return c.json({ ok: true, ignored: true }, 202);
317
+ }
318
+ if (isFeedbackEvent) {
319
+ const recorded = await handleFeedbackEvent(c, {
320
+ eventName,
321
+ payload,
322
+ repositoryId: delivery.repositoryId
323
+ });
324
+ return c.json({ ok: true, feedback: true, recorded }, 202);
325
+ }
326
+ const normalized = c.env.deps.webhook.normalizePayload(eventName, payload);
327
+ if (!normalized) {
328
+ return c.json({ ok: true, ignored: true, eventName }, 202);
329
+ }
330
+ const repoConfig = await c.env.deps.config.loadRepoConfig({
331
+ installationId,
332
+ owner: payload.repository.owner.login,
333
+ repo: payload.repository.name
334
+ });
335
+ if (repoConfig.enabled === false) {
336
+ return c.json({ ok: true, ignored: true, reason: "repository_disabled" }, 202);
337
+ }
338
+ const extracted = c.env.deps.webhook.extractReviewRequest({
339
+ eventName: normalized.eventName,
340
+ payload: normalized.payload,
341
+ botUsername: c.env.BOT_USERNAME,
342
+ config: repoConfig.parsedJson
343
+ });
344
+ if (extracted?.commitSha && extracted.baseSha) {
345
+ const jobsRepo = c.env.deps.repositories.jobs;
346
+ const existingJob = await jobsRepo.findExistingJobForHead(c.env, {
347
+ owner: extracted.owner,
348
+ repo: extracted.repo,
349
+ prNumber: extracted.prNumber,
350
+ commitSha: extracted.commitSha,
351
+ trigger: extracted.trigger
352
+ });
353
+ if (existingJob) {
354
+ return c.json({
355
+ ok: true,
356
+ duplicate: true,
357
+ message: existingJob.status === "queued" ? "queued" : "duplicate",
358
+ job: existingJob
359
+ }, 202);
360
+ }
361
+ const job = await jobsRepo.insertJob(c.env, {
362
+ installationId: extracted.installationId,
363
+ owner: extracted.owner,
364
+ repo: extracted.repo,
365
+ prNumber: extracted.prNumber,
366
+ prTitle: extracted.prTitle,
367
+ prAuthor: extracted.prAuthor,
368
+ commitSha: extracted.commitSha,
369
+ baseSha: extracted.baseSha,
370
+ trigger: extracted.trigger,
371
+ headRef: extracted.headRef,
372
+ baseRef: extracted.baseRef,
373
+ configSnapshot: repoConfig.parsedJson
374
+ });
375
+ await jobsRepo.supersedeOlderJobs(c.env, {
376
+ installationId: extracted.installationId,
377
+ owner: extracted.owner,
378
+ repo: extracted.repo,
379
+ prNumber: extracted.prNumber,
380
+ newJobId: job.id
381
+ });
382
+ await c.env.deps.platform.enqueueReviewJob({
383
+ jobId: job.id,
384
+ deliveryId,
385
+ phase: "prepare",
386
+ requestId: c.get("requestId")
387
+ });
388
+ return c.json({ ok: true, message: "queued", job }, 202);
389
+ }
390
+ await c.env.deps.platform.enqueueReviewJob({
391
+ deliveryId,
392
+ eventName,
393
+ requestId: c.get("requestId")
394
+ });
395
+ return c.json({ ok: true, message: "queued" }, 202);
396
+ }
397
+ function createWebhookRouter() {
398
+ const app = new Hono2();
399
+ app.post("/", handleGitHubWebhook);
400
+ return app;
401
+ }
402
+
403
+ // src/routes/api/auth.ts
404
+ import { isSupportedTimeZone } from "@codraoss/schema/timezone";
405
+ import { Hono as Hono3 } from "hono";
406
+ import { z } from "zod";
407
+ var emailSchema = z.strictObject({
408
+ email: z.string().trim().email().max(254)
409
+ });
410
+ var accountUpdateSchema = z.strictObject({
411
+ name: z.string().trim().min(1).max(120).optional(),
412
+ timezone: z.string().trim().min(1).max(64).refine(isSupportedTimeZone, {
413
+ message: "Unknown time zone."
414
+ }).nullable().optional()
415
+ }).refine(
416
+ (body) => body.name !== void 0 || body.timezone !== void 0,
417
+ { message: "Nothing to update." }
418
+ );
419
+ function createAuthApiRouter() {
420
+ const app = new Hono3();
421
+ app.get("/session", async (c) => {
422
+ const sessionUser = c.get("sessionUser");
423
+ if (!sessionUser) {
424
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
425
+ }
426
+ return c.json({ user: sessionUser });
427
+ });
428
+ app.get("/account", async (c) => {
429
+ const sessionUser = c.get("sessionUser");
430
+ if (!sessionUser) {
431
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
432
+ }
433
+ const accounts = c.env.deps.repositories.accounts;
434
+ const githubUserId = Number(sessionUser.providerUserId);
435
+ let account = await accounts.getAccountSettings(c.env, githubUserId);
436
+ if (!account) {
437
+ account = await accounts.upsertAccountSettings(c.env, {
438
+ githubUserId,
439
+ githubUsername: sessionUser.login,
440
+ accountName: sessionUser.name,
441
+ accountEmail: sessionUser.email
442
+ });
443
+ }
444
+ return c.json({ account });
445
+ });
446
+ app.patch("/account", async (c) => {
447
+ const sessionUser = c.get("sessionUser");
448
+ if (!sessionUser) {
449
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
450
+ }
451
+ const body = await c.req.json().catch(() => null);
452
+ const parsed = accountUpdateSchema.safeParse(body);
453
+ if (!parsed.success) {
454
+ const issue = parsed.error.issues[0]?.message;
455
+ return jsonError(
456
+ issue && issue !== "Invalid input" ? issue : "Enter a name (1-120 characters).",
457
+ 400
458
+ );
459
+ }
460
+ const accounts = c.env.deps.repositories.accounts;
461
+ const githubUserId = Number(sessionUser.providerUserId);
462
+ const existing = await accounts.getAccountSettings(c.env, githubUserId);
463
+ if (!existing) {
464
+ await accounts.upsertAccountSettings(c.env, {
465
+ githubUserId,
466
+ githubUsername: sessionUser.login,
467
+ accountName: sessionUser.name,
468
+ accountEmail: sessionUser.email
469
+ });
470
+ }
471
+ const account = await accounts.updateAccountSettings(c.env, githubUserId, {
472
+ accountName: parsed.data.name,
473
+ timezone: parsed.data.timezone
474
+ });
475
+ return c.json({ account });
476
+ });
477
+ app.get("/updates-email", async (c) => {
478
+ const sessionUser = c.get("sessionUser");
479
+ if (!sessionUser) {
480
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
481
+ }
482
+ const githubUserId = Number(sessionUser.providerUserId);
483
+ const preference = await c.env.deps.platform.getUpdatesEmailPreference(githubUserId);
484
+ return c.json({
485
+ status: preference?.status ?? "pending",
486
+ email: preference?.email ?? null,
487
+ updatedAt: preference?.updatedAt ?? null
488
+ });
489
+ });
490
+ app.post("/updates-email", async (c) => {
491
+ const sessionUser = c.get("sessionUser");
492
+ if (!sessionUser) {
493
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
494
+ }
495
+ const body = await c.req.json().catch(() => null);
496
+ const parsed = emailSchema.safeParse(body);
497
+ if (!parsed.success) {
498
+ return jsonError("Enter a valid email address.", 400);
499
+ }
500
+ const platform = c.env.deps.platform;
501
+ const githubUserId = Number(sessionUser.providerUserId);
502
+ const existingPreference = await platform.getUpdatesEmailPreference(githubUserId);
503
+ if (existingPreference) {
504
+ return c.json({
505
+ status: existingPreference.status,
506
+ email: existingPreference.email,
507
+ updatedAt: existingPreference.updatedAt
508
+ });
509
+ }
510
+ const synced = await platform.syncUpdatesEmail(githubUserId, parsed.data.email);
511
+ if (!synced) {
512
+ return jsonError("Could not save updates email right now.", 502);
513
+ }
514
+ const preference = await platform.getUpdatesEmailPreference(githubUserId);
515
+ return c.json({
516
+ status: preference?.status ?? "pending",
517
+ email: preference?.email ?? null,
518
+ updatedAt: preference?.updatedAt ?? null
519
+ });
520
+ });
521
+ return app;
522
+ }
523
+
524
+ // src/routes/api/jobs.ts
525
+ import { Hono as Hono4 } from "hono";
526
+ import { defaultRepoConfig, findingLabelSchema, jobsQuerySchema } from "@codraoss/schema";
527
+ import { parseUnifiedDiff } from "@codraoss/core/diff";
528
+ import { buildFileReviewPrompts } from "@codraoss/core/prompts/file-review";
529
+ async function terminateJobWorkflow(c, job) {
530
+ await c.env.deps.platform.terminateJobWorkflow(job);
531
+ }
532
+ function jobEtag(input) {
533
+ return `"job-${input.id}-${input.status}-${input.fileCount}-${input.commentCount}-${new Date(input.updatedAt).getTime()}"`;
534
+ }
535
+ function getExecutionContext(c) {
536
+ try {
537
+ return c.executionCtx;
538
+ } catch {
539
+ return void 0;
540
+ }
541
+ }
542
+ function createJobsRouter() {
543
+ const app = new Hono4();
544
+ app.get("/", async (c) => {
545
+ c.env.deps.platform.scheduleBestEffortJobMaintenance(getExecutionContext(c));
546
+ const rawQuery = c.req.query();
547
+ const query = jobsQuerySchema.parse(rawQuery);
548
+ const result = await c.env.deps.repositories.jobs.listJobs(c.env, query);
549
+ return c.json(result);
550
+ });
551
+ app.get("/:id", async (c) => {
552
+ c.env.deps.platform.scheduleBestEffortJobMaintenance(getExecutionContext(c));
553
+ const job = await c.env.deps.repositories.jobs.getJobDetail(c.env, c.req.param("id"));
554
+ if (!job) {
555
+ return jsonError("Job not found.", 404);
556
+ }
557
+ const etag = jobEtag(job);
558
+ const lastModified = new Date(job.updatedAt).toUTCString();
559
+ if (c.req.header("if-none-match") === etag) {
560
+ return new Response(null, {
561
+ status: 304,
562
+ headers: {
563
+ ETag: etag,
564
+ "Last-Modified": lastModified
565
+ }
566
+ });
567
+ }
568
+ const response = c.json({ job });
569
+ response.headers.set("ETag", etag);
570
+ response.headers.set("Last-Modified", lastModified);
571
+ response.headers.set("Cache-Control", "private, no-cache");
572
+ return response;
573
+ });
574
+ app.get("/:id/diffs", async (c) => {
575
+ const job = await c.env.deps.repositories.jobs.getJobDetail(c.env, c.req.param("id"));
576
+ if (!job) {
577
+ return jsonError("Job not found.", 404);
578
+ }
579
+ const config = job.configSnapshot ?? defaultRepoConfig;
580
+ const github = c.env.deps.gitProvider.createService(job.installationId);
581
+ let rawDiff;
582
+ try {
583
+ rawDiff = await c.env.deps.platform.getOrFetchRawDiffForCompletedJob(
584
+ c.env.deps.platform.createReviewRuntime(),
585
+ { id: job.id, owner: job.owner, repo: job.repo, baseSha: job.baseSha, commitSha: job.commitSha },
586
+ github
587
+ );
588
+ } catch (error) {
589
+ c.env.deps.platform.logger.warn(`Could not reconstruct diff for job ${job.id}`, error instanceof Error ? error : new Error(String(error)));
590
+ return c.json({ diffs: {} });
591
+ }
592
+ let prDescription = null;
593
+ try {
594
+ prDescription = (await github.getPullRequest(job.owner, job.repo, job.prNumber)).body ?? null;
595
+ } catch (error) {
596
+ c.env.deps.platform.logger.warn(
597
+ `Could not load the PR body for job ${job.id}; prompts will omit the description`,
598
+ error instanceof Error ? error : new Error(String(error))
599
+ );
600
+ }
601
+ const diffs = {};
602
+ for (const file of parseUnifiedDiff(rawDiff, config.review)) {
603
+ if (file.isDeleted || file.isBinary || !file.path) continue;
604
+ diffs[file.path] = buildFileReviewPrompts({
605
+ file,
606
+ prTitle: job.prTitle,
607
+ prDescription,
608
+ config: config.review
609
+ }).userPrompt;
610
+ }
611
+ const response = c.json({ diffs });
612
+ response.headers.set("Cache-Control", "private, max-age=60");
613
+ return response;
614
+ });
615
+ async function startReplacementJob(c, rawSource, options) {
616
+ const jobs = c.env.deps.repositories.jobs;
617
+ const source = jobs.mapJob(rawSource);
618
+ let configSnapshot;
619
+ try {
620
+ const currentConfig = await c.env.deps.config.loadRepoConfig({
621
+ installationId: source.installationId,
622
+ owner: source.owner,
623
+ repo: source.repo
624
+ });
625
+ configSnapshot = currentConfig?.parsedJson ?? defaultRepoConfig;
626
+ } catch (e) {
627
+ configSnapshot = defaultRepoConfig;
628
+ }
629
+ const job = await jobs.insertJob(c.env, {
630
+ installationId: source.installationId,
631
+ owner: source.owner,
632
+ repo: source.repo,
633
+ prNumber: source.prNumber,
634
+ prTitle: source.prTitle,
635
+ prAuthor: source.prAuthor,
636
+ commitSha: source.commitSha,
637
+ baseSha: jobs.bytesToHex(rawSource.base_sha),
638
+ // base_sha is only in raw row/detail
639
+ trigger: "retry",
640
+ headRef: rawSource.head_ref,
641
+ baseRef: rawSource.base_ref,
642
+ configSnapshot,
643
+ ...options.inherit ? { retryOfJobId: source.id } : {}
644
+ });
645
+ await jobs.supersedeOlderJobs(c.env, {
646
+ installationId: source.installationId,
647
+ owner: source.owner,
648
+ repo: source.repo,
649
+ prNumber: source.prNumber,
650
+ newJobId: job.id
651
+ });
652
+ await c.env.deps.platform.enqueueReviewJob({
653
+ jobId: job.id,
654
+ deliveryId: crypto.randomUUID(),
655
+ phase: "prepare",
656
+ requestId: c.get("requestId")
657
+ });
658
+ return job;
659
+ }
660
+ app.post("/:id/retry", async (c) => {
661
+ const jobs = c.env.deps.repositories.jobs;
662
+ const rawSource = await jobs.getJobForProcessing(c.env, c.req.param("id"));
663
+ if (!rawSource) {
664
+ return jsonError("Job not found.", 404);
665
+ }
666
+ const job = await startReplacementJob(c, rawSource, { inherit: true });
667
+ return c.json({ job }, 202);
668
+ });
669
+ app.post("/:id/rerun", async (c) => {
670
+ const jobs = c.env.deps.repositories.jobs;
671
+ const rawSource = await jobs.getJobForProcessing(c.env, c.req.param("id"));
672
+ if (!rawSource) {
673
+ return jsonError("Job not found.", 404);
674
+ }
675
+ const source = jobs.mapJob(rawSource);
676
+ if (source.status === "queued" || source.status === "running") {
677
+ await terminateJobWorkflow(c, source);
678
+ }
679
+ const job = await startReplacementJob(c, rawSource, { inherit: false });
680
+ return c.json({ job }, 202);
681
+ });
682
+ app.post("/:id/stop", async (c) => {
683
+ const jobs = c.env.deps.repositories.jobs;
684
+ const id = c.req.param("id");
685
+ const raw = await jobs.getJobForProcessing(c.env, id);
686
+ if (!raw) {
687
+ return jsonError("Job not found.", 404);
688
+ }
689
+ const job = jobs.mapJob(raw);
690
+ if (job.status !== "queued" && job.status !== "running") {
691
+ return jsonError("Only a queued or running job can be stopped.", 409);
692
+ }
693
+ await terminateJobWorkflow(c, job);
694
+ await jobs.cancelJob(c.env, id);
695
+ const updated = await jobs.getJobForProcessing(c.env, id);
696
+ return c.json({ job: updated ? jobs.mapJob(updated) : job }, 200);
697
+ });
698
+ app.put("/:id/findings/:fingerprint/label", async (c) => {
699
+ const jobId = c.req.param("id");
700
+ const fingerprint = c.req.param("fingerprint");
701
+ const parsed = findingLabelSchema.safeParse(await c.req.json().catch(() => null));
702
+ if (!parsed.success) return jsonError('Body must be {"label":"right"|"wrong"}.', 400);
703
+ const target = await c.env.deps.repositories.fileReviews.getFindingLabelTarget(c.env, jobId, fingerprint);
704
+ if (!target) return jsonError("Finding not found on this job.", 404);
705
+ await c.env.deps.repositories.commentFeedback.upsertDashboardFeedback(c.env, {
706
+ repositoryId: target.repository_id,
707
+ prNumber: target.pr_number,
708
+ fingerprint,
709
+ anchorHash: target.anchor_hash,
710
+ // Carried so a rejection survives the model rewording its title.
711
+ fingerprintV2: target.fingerprint_v2,
712
+ jobId,
713
+ labelledBy: c.get("sessionUser")?.providerUserId ? Number(c.get("sessionUser")?.providerUserId) : null,
714
+ outcome: parsed.data.label === "wrong" ? "marked_wrong" : "marked_right"
715
+ });
716
+ return c.json({ label: parsed.data.label }, 200);
717
+ });
718
+ app.delete("/:id/findings/:fingerprint/label", async (c) => {
719
+ const jobId = c.req.param("id");
720
+ const fingerprint = c.req.param("fingerprint");
721
+ const target = await c.env.deps.repositories.fileReviews.getFindingLabelTarget(c.env, jobId, fingerprint);
722
+ if (!target) return jsonError("Finding not found on this job.", 404);
723
+ await c.env.deps.repositories.commentFeedback.clearDashboardFeedback(c.env, target.repository_id, fingerprint);
724
+ return c.body(null, 204);
725
+ });
726
+ app.delete("/:id", async (c) => {
727
+ const jobs = c.env.deps.repositories.jobs;
728
+ const id = c.req.param("id");
729
+ const raw = await jobs.getJobForProcessing(c.env, id);
730
+ if (!raw) {
731
+ return jsonError("Job not found.", 404);
732
+ }
733
+ const job = jobs.mapJob(raw);
734
+ if (job.status === "queued" || job.status === "running") {
735
+ await terminateJobWorkflow(c, job);
736
+ }
737
+ await jobs.deleteJob(c.env, id);
738
+ return c.body(null, 204);
739
+ });
740
+ return app;
741
+ }
742
+
743
+ // src/routes/api/repos.ts
744
+ import { Hono as Hono5 } from "hono";
745
+ import { z as z2 } from "zod";
746
+ import { repoConfigSchema } from "@codraoss/schema";
747
+ var repoConfigPatchSchema = z2.strictObject({
748
+ enabled: z2.boolean().optional(),
749
+ review: repoConfigSchema.shape.review.optional(),
750
+ model: repoConfigSchema.shape.model.optional()
751
+ }).refine(
752
+ (patch) => patch.enabled !== void 0 || patch.review !== void 0 || patch.model !== void 0,
753
+ "Repository config patch cannot be empty."
754
+ );
755
+ async function mapWithConcurrency(items, limit, mapper) {
756
+ const results = new Array(items.length);
757
+ let nextIndex = 0;
758
+ async function worker() {
759
+ while (nextIndex < items.length) {
760
+ const currentIndex = nextIndex;
761
+ nextIndex += 1;
762
+ results[currentIndex] = await mapper(items[currentIndex]);
763
+ }
764
+ }
765
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
766
+ return results;
767
+ }
768
+ function createReposRouter() {
769
+ const app = new Hono5();
770
+ app.get("/", async (c) => {
771
+ const repos = await c.env.deps.repositories.repoConfigs.listRepoConfigs(c.env);
772
+ return c.json({ repos });
773
+ });
774
+ app.get("/install", async (c) => {
775
+ try {
776
+ return c.redirect(await c.env.deps.gitProvider.getAppInstallationUrl(), 302);
777
+ } catch (error) {
778
+ c.env.deps.platform.logger.error("Failed to resolve GitHub App installation URL:", error);
779
+ return jsonError(`Failed to resolve GitHub App installation URL: ${error instanceof Error ? error.message : String(error)}`, 500);
780
+ }
781
+ });
782
+ app.post("/sync", async (c) => {
783
+ try {
784
+ const installations = await c.env.deps.gitProvider.listInstallations();
785
+ const synced = [];
786
+ const repoConfigs = c.env.deps.repositories.repoConfigs;
787
+ for (const inst of installations) {
788
+ const github = c.env.deps.gitProvider.createService(String(inst.id));
789
+ const repos = await github.listRepositories();
790
+ const results = await mapWithConcurrency(
791
+ repos,
792
+ 5,
793
+ async (repo) => {
794
+ const owner = repo.owner.login;
795
+ const name = repo.name;
796
+ const fullName = `${owner}/${name}`;
797
+ try {
798
+ await repoConfigs.syncRepoConfig(c.env, {
799
+ installationId: String(inst.id),
800
+ owner,
801
+ repo: name
802
+ });
803
+ return fullName;
804
+ } catch (repoError) {
805
+ c.env.deps.platform.logger.error(`Failed to sync repo: ${fullName}`, repoError);
806
+ return null;
807
+ }
808
+ }
809
+ );
810
+ const installationSynced = [];
811
+ for (const res of results) {
812
+ if (res) {
813
+ synced.push(res);
814
+ installationSynced.push(res);
815
+ }
816
+ }
817
+ await repoConfigs.deleteStaleRepoConfigs(c.env, String(inst.id), installationSynced);
818
+ }
819
+ return c.json({ ok: true, synced });
820
+ } catch (error) {
821
+ c.env.deps.platform.logger.error("Manual sync failed:", error);
822
+ return jsonError(`Sync failed: ${error instanceof Error ? error.message : String(error)}`, 500);
823
+ }
824
+ });
825
+ app.get("/:owner/:repo/config", async (c) => {
826
+ const repo = await c.env.deps.repositories.repoConfigs.getRepoConfigRecord(c.env, c.req.param("owner"), c.req.param("repo"));
827
+ if (!repo) {
828
+ return jsonError("Repository config not found.", 404);
829
+ }
830
+ return c.json({ repo });
831
+ });
832
+ app.patch("/:owner/:repo/config", async (c) => {
833
+ const { owner, repo } = c.req.param();
834
+ const body = await c.req.json();
835
+ const parsedPatch = repoConfigPatchSchema.safeParse(body);
836
+ if (!parsedPatch.success) {
837
+ return jsonError("Invalid repository config patch.", 400);
838
+ }
839
+ const repoConfigs = c.env.deps.repositories.repoConfigs;
840
+ const existing = await repoConfigs.getRepoConfigRecord(c.env, owner, repo);
841
+ if (!existing) {
842
+ return jsonError("Repository config not found.", 404);
843
+ }
844
+ const patch = parsedPatch.data;
845
+ const hasConfigPatch = patch.review !== void 0 || patch.model !== void 0;
846
+ if (!hasConfigPatch && patch.enabled !== void 0) {
847
+ await repoConfigs.updateRepoConfigEnabled(c.env, {
848
+ owner,
849
+ repo,
850
+ enabled: patch.enabled
851
+ });
852
+ await c.env.deps.config.invalidateRepoConfigCache(owner, repo);
853
+ return c.json({ ok: true });
854
+ }
855
+ const configPatch = {};
856
+ if (patch.review !== void 0) {
857
+ configPatch.review = patch.review;
858
+ }
859
+ if (patch.model !== void 0) {
860
+ configPatch.model = patch.model;
861
+ }
862
+ const updatedParsedJson = {
863
+ ...existing.parsedJson,
864
+ ...configPatch
865
+ };
866
+ const parsedConfig = repoConfigSchema.safeParse(updatedParsedJson);
867
+ if (!parsedConfig.success) {
868
+ return jsonError("Invalid repository config.", 400);
869
+ }
870
+ await repoConfigs.upsertRepoConfig(c.env, {
871
+ installationId: existing.installationId,
872
+ owner,
873
+ repo,
874
+ parsedJson: parsedConfig.data,
875
+ enabled: patch.enabled
876
+ });
877
+ await c.env.deps.config.invalidateRepoConfigCache(owner, repo);
878
+ return c.json({ ok: true });
879
+ });
880
+ return app;
881
+ }
882
+
883
+ // src/routes/api/stats.ts
884
+ import { Hono as Hono6 } from "hono";
885
+ function createStatsRouter() {
886
+ const app = new Hono6();
887
+ app.get("/", async (c) => {
888
+ const daysParam = c.req.query("days");
889
+ const days = daysParam ? parseInt(daysParam, 10) : 30;
890
+ const timeZone = c.req.query("tz") ?? "UTC";
891
+ const stats = await c.env.deps.repositories.stats.getStats(c.env, days, timeZone);
892
+ return c.json({ stats });
893
+ });
894
+ return app;
895
+ }
896
+
897
+ // src/routes/api/models.ts
898
+ import { Hono as Hono7 } from "hono";
899
+ import { z as z3 } from "zod";
900
+ import { llmApiFormats } from "@codraoss/schema";
901
+ var apiFormatSchema = z3.enum(llmApiFormats);
902
+ var positiveIntegerSchema = z3.number().int().positive().finite();
903
+ var modelIdSchema = z3.string().trim().min(1);
904
+ var optionalUrlSchema = z3.string().trim().url().nullable().optional();
905
+ var providerIdSchema = z3.string().uuid();
906
+ var providerCreateSchema = z3.strictObject({
907
+ name: z3.string().trim().min(1),
908
+ apiFormat: apiFormatSchema,
909
+ baseUrl: optionalUrlSchema,
910
+ apiKey: z3.string().optional(),
911
+ enabled: z3.boolean().default(true)
912
+ });
913
+ var providerUpdateSchema = providerCreateSchema.extend({
914
+ clearApiKey: z3.boolean().optional()
915
+ });
916
+ var modelConfigUpdateSchema = z3.strictObject({
917
+ providerId: providerIdSchema,
918
+ modelName: z3.string().trim().min(1)
919
+ });
920
+ var globalModelConfigSchema = z3.strictObject({
921
+ main: modelIdSchema.nullable().default(null),
922
+ fallbacks: z3.array(modelIdSchema).nullable().default([]),
923
+ size_overrides: z3.array(
924
+ z3.strictObject({
925
+ max_lines: positiveIntegerSchema,
926
+ model: modelIdSchema,
927
+ fallbacks: z3.array(modelIdSchema).optional()
928
+ })
929
+ ).nullable().optional()
930
+ });
931
+ function requiresExplicitBaseUrl(apiFormat, baseUrl) {
932
+ return apiFormat === "vertex" && !baseUrl;
933
+ }
934
+ function readModelIdParam(value) {
935
+ try {
936
+ return decodeURIComponent(value);
937
+ } catch {
938
+ return value;
939
+ }
940
+ }
941
+ function createModelsRouter() {
942
+ const app = new Hono7();
943
+ app.get("/", async (c) => {
944
+ const modelConfigsRepo = c.env.deps.repositories.modelConfigs;
945
+ const [providers, configs] = await Promise.all([
946
+ modelConfigsRepo.listLlmProviders(c.env),
947
+ modelConfigsRepo.listModelConfigs(c.env)
948
+ ]);
949
+ return c.json({ providers, configs });
950
+ });
951
+ app.post("/sync", async (c) => {
952
+ const modelConfigsRepo = c.env.deps.repositories.modelConfigs;
953
+ const syncErrors = await c.env.deps.modelRunner.syncProviderModelCatalog();
954
+ const [providers, configs] = await Promise.all([
955
+ modelConfigsRepo.listLlmProviders(c.env),
956
+ modelConfigsRepo.listModelConfigs(c.env)
957
+ ]);
958
+ return c.json({ providers, configs, syncErrors });
959
+ });
960
+ app.get("/global", async (c) => {
961
+ const config = await c.env.deps.config.getGlobalConfig();
962
+ return c.json({ config });
963
+ });
964
+ app.patch("/global", async (c) => {
965
+ const body = await c.req.json();
966
+ const parsed = globalModelConfigSchema.safeParse(body);
967
+ if (!parsed.success) {
968
+ return jsonError("Invalid global model config.", 400);
969
+ }
970
+ await c.env.deps.config.updateGlobalConfig(parsed.data);
971
+ return c.json({ ok: true });
972
+ });
973
+ app.post("/providers", async (c) => {
974
+ const parsed = providerCreateSchema.safeParse(await c.req.json());
975
+ if (!parsed.success) {
976
+ return jsonError("Invalid provider config.", 400);
977
+ }
978
+ const input = parsed.data;
979
+ if (requiresExplicitBaseUrl(input.apiFormat, input.baseUrl)) {
980
+ return jsonError("Vertex AI requires a base URL with your GCP project ID and region, e.g. https://us-central1-aiplatform.googleapis.com/v1/projects/YOUR_PROJECT_ID/locations/us-central1", 400);
981
+ }
982
+ try {
983
+ const provider = await c.env.deps.modelRunner.createProviderWithSecret(input);
984
+ return c.json({ provider }, 201);
985
+ } catch (error) {
986
+ const err = error;
987
+ if (err.isUniqueNameError) {
988
+ return jsonError(`Provider ${input.name} already exists. Update the existing provider instead.`, 409);
989
+ }
990
+ if (err.isEncryptionConfigError) {
991
+ return jsonError(err.message || "LLM encryption is not configured.", 400);
992
+ }
993
+ if (err.isKeyRequiredError) {
994
+ return jsonError(`Provider ${input.name} needs an API key before it can be enabled.`, 400);
995
+ }
996
+ throw error;
997
+ }
998
+ });
999
+ app.patch("/providers/:id", async (c) => {
1000
+ const id = c.req.param("id");
1001
+ if (!providerIdSchema.safeParse(id).success) {
1002
+ return jsonError("Invalid provider id.", 400);
1003
+ }
1004
+ const parsed = providerUpdateSchema.safeParse(await c.req.json());
1005
+ if (!parsed.success) {
1006
+ return jsonError("Invalid provider config.", 400);
1007
+ }
1008
+ const input = parsed.data;
1009
+ if (requiresExplicitBaseUrl(input.apiFormat, input.baseUrl)) {
1010
+ return jsonError("Vertex AI requires a base URL with your GCP project ID and region, e.g. https://us-central1-aiplatform.googleapis.com/v1/projects/YOUR_PROJECT_ID/locations/us-central1", 400);
1011
+ }
1012
+ try {
1013
+ const provider = await c.env.deps.modelRunner.updateProviderWithSecret(id, input);
1014
+ if (!provider) return jsonError("Provider not found.", 404);
1015
+ return c.json({ provider });
1016
+ } catch (error) {
1017
+ const err = error;
1018
+ if (err.isUniqueNameError) {
1019
+ return jsonError(`Provider ${input.name} already exists. Choose a different provider name.`, 409);
1020
+ }
1021
+ if (err.isEncryptionConfigError) {
1022
+ return jsonError(err.message || "LLM encryption is not configured.", 400);
1023
+ }
1024
+ if (err.isKeyRequiredError) {
1025
+ return jsonError(`Provider ${input.name} needs an API key before it can be enabled.`, 400);
1026
+ }
1027
+ throw error;
1028
+ }
1029
+ });
1030
+ app.delete("/providers/:id", async (c) => {
1031
+ const id = c.req.param("id");
1032
+ if (!providerIdSchema.safeParse(id).success) {
1033
+ return jsonError("Invalid provider id.", 400);
1034
+ }
1035
+ const result = await c.env.deps.repositories.modelConfigs.deleteLlmProvider(c.env, id);
1036
+ if (!result.deleted) {
1037
+ return jsonError(result.reason ?? "Provider not found.", result.reason ? 409 : 404);
1038
+ }
1039
+ return c.json({ ok: true });
1040
+ });
1041
+ app.post("/:id/test", async (c) => {
1042
+ const modelId = readModelIdParam(c.req.param("id"));
1043
+ const parsedModelId = modelIdSchema.safeParse(modelId);
1044
+ if (!parsedModelId.success) {
1045
+ return jsonError("Invalid model id.", 400);
1046
+ }
1047
+ try {
1048
+ const response = await c.env.deps.modelRunner.testConnection(parsedModelId.data);
1049
+ return c.json(response);
1050
+ } catch (error) {
1051
+ const err = error;
1052
+ if (err.isNotFoundError) return jsonError("Model not found.", 404);
1053
+ if (err.isDisabledError || err.isMissingKeyError) return jsonError(err.message, 400);
1054
+ return jsonError(
1055
+ err.message || "Connection test failed.",
1056
+ err.status || 502
1057
+ );
1058
+ }
1059
+ });
1060
+ app.post("/:id", async (c) => {
1061
+ const modelId = readModelIdParam(c.req.param("id"));
1062
+ const parsedModelId = modelIdSchema.safeParse(modelId);
1063
+ if (!parsedModelId.success) {
1064
+ return jsonError("Invalid model id.", 400);
1065
+ }
1066
+ const body = await c.req.json();
1067
+ const parsed = modelConfigUpdateSchema.safeParse(body);
1068
+ if (!parsed.success) {
1069
+ return jsonError("Invalid model config.", 400);
1070
+ }
1071
+ const saved = await c.env.deps.repositories.modelConfigs.updateModelConfig(c.env, {
1072
+ modelId: parsedModelId.data,
1073
+ providerId: parsed.data.providerId,
1074
+ modelName: parsed.data.modelName
1075
+ });
1076
+ if (!saved) return jsonError("Provider not found.", 404);
1077
+ return c.json({ ok: true, config: saved });
1078
+ });
1079
+ app.delete("/:id", async (c) => {
1080
+ const modelId = readModelIdParam(c.req.param("id"));
1081
+ const parsedModelId = modelIdSchema.safeParse(modelId);
1082
+ if (!parsedModelId.success) {
1083
+ return jsonError("Invalid model id.", 400);
1084
+ }
1085
+ const deleted = await c.env.deps.repositories.modelConfigs.deleteModelConfig(c.env, parsedModelId.data);
1086
+ if (!deleted) return jsonError("Model not found.", 404);
1087
+ return c.json({ ok: true });
1088
+ });
1089
+ return app;
1090
+ }
1091
+
1092
+ // src/routes/api/settings.ts
1093
+ import { Hono as Hono8 } from "hono";
1094
+ import { z as z4 } from "zod";
1095
+ import { reviewConcurrencyLevels, reviewMaxCommentsOptions, reviewMaxFilesRange, reviewSettingsSchema } from "@codraoss/schema";
1096
+ var reviewSettingsPatchSchema = z4.strictObject({
1097
+ concurrencyLevel: z4.enum(reviewConcurrencyLevels).optional(),
1098
+ maxComments: z4.number().int().refine(
1099
+ (value) => reviewMaxCommentsOptions.includes(value),
1100
+ "Invalid max comments value."
1101
+ ).optional(),
1102
+ maxFiles: z4.number().int().min(reviewMaxFilesRange.min).max(reviewMaxFilesRange.max).optional()
1103
+ }).refine(
1104
+ (settings) => Object.values(settings).some((value) => value !== void 0),
1105
+ "At least one setting must be provided."
1106
+ );
1107
+ function createSettingsRouter() {
1108
+ const app = new Hono8();
1109
+ app.get("/", async (c) => {
1110
+ const settings = await c.env.deps.repositories.appSettings.getReviewSettings(c.env);
1111
+ return c.json({ settings });
1112
+ });
1113
+ app.patch("/", async (c) => {
1114
+ const body = await c.req.json().catch(() => null);
1115
+ const parsed = reviewSettingsPatchSchema.safeParse(body);
1116
+ if (!parsed.success) {
1117
+ return jsonError("Invalid review settings.", 400);
1118
+ }
1119
+ const current = await c.env.deps.repositories.appSettings.getReviewSettings(c.env);
1120
+ const next = reviewSettingsSchema.parse({ ...current, ...parsed.data });
1121
+ await c.env.deps.repositories.appSettings.updateReviewSettings(c.env, next);
1122
+ return c.json({ ok: true, settings: next });
1123
+ });
1124
+ return app;
1125
+ }
1126
+
1127
+ // src/router.ts
1128
+ async function serveIndex(c) {
1129
+ const assetsFetch = c.env.ASSETS?.fetch;
1130
+ if (typeof assetsFetch === "function") {
1131
+ return assetsFetch(new URL("/index.html", c.req.url));
1132
+ }
1133
+ return c.text("Not Found: Please mount UI static assets handler here.", 404);
1134
+ }
1135
+ function createApiRouter() {
1136
+ const app = new Hono9();
1137
+ app.use("*", observability);
1138
+ app.use("/auth/logout", requireSession);
1139
+ app.use("/auth/logout", requireCsrfHeader);
1140
+ app.route("/auth", createAuthRouter());
1141
+ app.route("/webhook", createWebhookRouter());
1142
+ app.use("/api/*", requireSession);
1143
+ app.use("/api/*", requireCsrfHeader);
1144
+ app.route("/api/auth", createAuthApiRouter());
1145
+ app.route("/api/jobs", createJobsRouter());
1146
+ app.route("/api/repos", createReposRouter());
1147
+ app.route("/api/stats", createStatsRouter());
1148
+ app.route("/api/models", createModelsRouter());
1149
+ app.route("/api/settings", createSettingsRouter());
1150
+ app.get("/login", serveIndex);
1151
+ app.get("/", serveIndex);
1152
+ app.get("/dashboard", requireSession, serveIndex);
1153
+ app.get("/jobs", requireSession, serveIndex);
1154
+ app.get("/jobs/*", requireSession, serveIndex);
1155
+ app.get("/repos", requireSession, serveIndex);
1156
+ app.get("/stats", requireSession, serveIndex);
1157
+ app.get("/health", requireSession, serveIndex);
1158
+ app.get("/settings", requireSession, serveIndex);
1159
+ app.get("/account", requireSession, serveIndex);
1160
+ return app;
1161
+ }
1162
+ export {
1163
+ createApiRouter
1164
+ };
1165
+ //# sourceMappingURL=index.js.map