@codraoss/api 0.9.6 → 0.9.13

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.
@@ -0,0 +1,145 @@
1
+ // src/platform/config.ts
2
+ import { defaultRepoConfig, normalizeRepoModelConfig, repoConfigSchema } from "@codraoss/schema";
3
+ import { REPO_CONFIG_CACHE_VERSION } from "@codraoss/schema";
4
+ import { getRepoConfigRecord, syncRepoConfig } from "@codraoss/db/repo-configs";
5
+ var REPO_CONFIG_CACHE_PREFIX = `config:${REPO_CONFIG_CACHE_VERSION}:db:`;
6
+ var REPO_CONFIG_REVISION_KEY = `config:${REPO_CONFIG_CACHE_VERSION}:db_revision`;
7
+ async function getRepoConfigCacheRevision(kv) {
8
+ return await kv.get(REPO_CONFIG_REVISION_KEY) ?? "0";
9
+ }
10
+ async function cacheKey(kv, owner, repo) {
11
+ const revision = await getRepoConfigCacheRevision(kv);
12
+ return `${REPO_CONFIG_CACHE_PREFIX}${revision}:${owner}/${repo}`;
13
+ }
14
+ var GLOBAL_CONFIG_KEY = "config:global_model";
15
+ var EMPTY_GLOBAL_CONFIG = {
16
+ main: null,
17
+ fallbacks: [],
18
+ size_overrides: []
19
+ };
20
+ function hasRepoModelOverride(existing) {
21
+ return Boolean(
22
+ existing?.mainModel || Array.isArray(existing?.fallbackModels) && existing.fallbackModels.length > 0 || Array.isArray(existing?.sizeOverrides) && existing.sizeOverrides.length > 0
23
+ );
24
+ }
25
+ async function getGlobalConfig(kv) {
26
+ const cached = await kv.get(GLOBAL_CONFIG_KEY, "json");
27
+ if (cached) {
28
+ const parsed = repoConfigSchema.shape.model.safeParse(cached);
29
+ if (parsed.success) {
30
+ return normalizeRepoModelConfig(parsed.data);
31
+ }
32
+ }
33
+ return EMPTY_GLOBAL_CONFIG;
34
+ }
35
+ async function updateGlobalConfig(kv, config) {
36
+ await kv.put(GLOBAL_CONFIG_KEY, JSON.stringify(normalizeRepoModelConfig(config)));
37
+ await invalidateAllRepoConfigCache(kv);
38
+ }
39
+ async function invalidateRepoConfigCache(kv, owner, repo) {
40
+ await kv.delete(await cacheKey(kv, owner, repo));
41
+ }
42
+ async function invalidateAllRepoConfigCache(kv) {
43
+ await kv.put(REPO_CONFIG_REVISION_KEY, String(Date.now()));
44
+ }
45
+ async function loadRepoConfig(kv, db, input) {
46
+ const key = await cacheKey(kv, input.owner, input.repo);
47
+ const cached = await kv.get(key, "json");
48
+ if (cached) {
49
+ return cached;
50
+ }
51
+ const existing = await getRepoConfigRecord(db, input.owner, input.repo);
52
+ let parsedJson = existing?.parsedJson ?? defaultRepoConfig;
53
+ const enabled = existing?.enabled ?? true;
54
+ if (!hasRepoModelOverride(existing)) {
55
+ const globalModel = await getGlobalConfig(kv);
56
+ parsedJson = {
57
+ ...parsedJson,
58
+ model: globalModel
59
+ };
60
+ }
61
+ const finalConfig = {
62
+ parsedJson,
63
+ enabled
64
+ };
65
+ await kv.put(key, JSON.stringify(finalConfig), { expirationTtl: 60 * 10 });
66
+ if (!existing) {
67
+ await syncRepoConfig(db, input);
68
+ }
69
+ return finalConfig;
70
+ }
71
+
72
+ // src/platform/updates-email.ts
73
+ var EMAILS_API_URL = "https://codra.run/api/emails";
74
+ function updatesEmailKey(githubUserId) {
75
+ return `updates-email:${githubUserId}`;
76
+ }
77
+ async function getUpdatesEmailPreference(kv, githubUserId) {
78
+ return await kv.get(updatesEmailKey(githubUserId), "json");
79
+ }
80
+ async function hasUpdatesEmailPreference(kv, githubUserId) {
81
+ return Boolean(await getUpdatesEmailPreference(kv, githubUserId));
82
+ }
83
+ async function syncUpdatesEmail(kv, githubUserId, email) {
84
+ if (!email) return false;
85
+ if (await hasUpdatesEmailPreference(kv, githubUserId)) return false;
86
+ const response = await fetch(EMAILS_API_URL, {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json" },
89
+ body: JSON.stringify({ email })
90
+ });
91
+ if (!response.ok) {
92
+ console.warn("Failed to sync updates email", { status: response.status, url: response.url });
93
+ return false;
94
+ }
95
+ const record = {
96
+ status: "subscribed",
97
+ email,
98
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
99
+ };
100
+ await kv.put(updatesEmailKey(githubUserId), JSON.stringify(record));
101
+ return true;
102
+ }
103
+
104
+ // src/platform/oauth.ts
105
+ import { randomHex } from "@codraoss/schema/hex";
106
+ var OAUTH_STATE_TTL_SECONDS = 60 * 10;
107
+ function oauthStateKey(state) {
108
+ return `oauth-state:${state}`;
109
+ }
110
+ function parseAllowedUsers(input) {
111
+ return new Set(
112
+ input.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
113
+ );
114
+ }
115
+ async function createOAuthState(kv) {
116
+ const state = randomHex();
117
+ await kv.put(
118
+ oauthStateKey(state),
119
+ JSON.stringify({ createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
120
+ { expirationTtl: OAUTH_STATE_TTL_SECONDS }
121
+ );
122
+ return state;
123
+ }
124
+ async function consumeOAuthState(kv, state) {
125
+ const key = oauthStateKey(state);
126
+ const value = await kv.get(key);
127
+ if (!value) {
128
+ return false;
129
+ }
130
+ await kv.delete(key);
131
+ return true;
132
+ }
133
+
134
+ export {
135
+ getGlobalConfig,
136
+ updateGlobalConfig,
137
+ invalidateRepoConfigCache,
138
+ loadRepoConfig,
139
+ getUpdatesEmailPreference,
140
+ syncUpdatesEmail,
141
+ parseAllowedUsers,
142
+ createOAuthState,
143
+ consumeOAuthState
144
+ };
145
+ //# sourceMappingURL=chunk-N45ONQ2Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/platform/config.ts","../src/platform/updates-email.ts","../src/platform/oauth.ts"],"sourcesContent":["import { defaultRepoConfig, normalizeRepoModelConfig, repoConfigSchema, type RepoConfig } from '@codraoss/schema';\nimport { REPO_CONFIG_CACHE_VERSION } from '@codraoss/schema';\nimport { getRepoConfigRecord, syncRepoConfig } from '@codraoss/db/repo-configs';\nimport type { DbEnv } from '@codraoss/db/env';\nimport type { KvCompat } from './kv';\n\nexport type { KvCompat };\n\ntype CachedConfig = {\n parsedJson: RepoConfig;\n enabled: boolean;\n};\n\nconst REPO_CONFIG_CACHE_PREFIX = `config:${REPO_CONFIG_CACHE_VERSION}:db:`;\nconst REPO_CONFIG_REVISION_KEY = `config:${REPO_CONFIG_CACHE_VERSION}:db_revision`;\n\nasync function getRepoConfigCacheRevision(kv: KvCompat) {\n return (await kv.get(REPO_CONFIG_REVISION_KEY)) ?? '0';\n}\n\nasync function cacheKey(kv: KvCompat, owner: string, repo: string) {\n const revision = await getRepoConfigCacheRevision(kv);\n return `${REPO_CONFIG_CACHE_PREFIX}${revision}:${owner}/${repo}`;\n}\n\nconst GLOBAL_CONFIG_KEY = 'config:global_model';\n\nconst EMPTY_GLOBAL_CONFIG: RepoConfig['model'] = {\n main: null,\n fallbacks: [],\n size_overrides: [],\n};\n\nfunction hasRepoModelOverride(existing: Awaited<ReturnType<typeof getRepoConfigRecord>> | null) {\n return Boolean(\n existing?.mainModel ||\n (Array.isArray(existing?.fallbackModels) && existing.fallbackModels.length > 0) ||\n (Array.isArray(existing?.sizeOverrides) && existing.sizeOverrides.length > 0),\n );\n}\n\nexport async function getGlobalConfig(kv: KvCompat): Promise<RepoConfig['model']> {\n const cached = await kv.get(GLOBAL_CONFIG_KEY, 'json');\n if (cached) {\n const parsed = repoConfigSchema.shape.model.safeParse(cached);\n if (parsed.success) {\n return normalizeRepoModelConfig(parsed.data);\n }\n }\n\n return EMPTY_GLOBAL_CONFIG;\n}\n\nexport async function updateGlobalConfig(kv: KvCompat, config: RepoConfig['model']) {\n await kv.put(GLOBAL_CONFIG_KEY, JSON.stringify(normalizeRepoModelConfig(config)));\n await invalidateAllRepoConfigCache(kv);\n}\n\nexport async function invalidateRepoConfigCache(kv: KvCompat, owner: string, repo: string) {\n await kv.delete(await cacheKey(kv, owner, repo));\n}\n\nasync function invalidateAllRepoConfigCache(kv: KvCompat) {\n await kv.put(REPO_CONFIG_REVISION_KEY, String(Date.now()));\n}\n\n\nexport async function loadRepoConfig(\n kv: KvCompat,\n db: DbEnv,\n input: { installationId: string; owner: string; repo: string },\n) {\n const key = await cacheKey(kv, input.owner, input.repo);\n const cached = await kv.get(key, 'json');\n if (cached) {\n return cached as CachedConfig;\n }\n\n const existing = await getRepoConfigRecord(db, input.owner, input.repo);\n\n let parsedJson = existing?.parsedJson ?? defaultRepoConfig;\n const enabled = existing?.enabled ?? true;\n\n if (!hasRepoModelOverride(existing)) {\n const globalModel = await getGlobalConfig(kv);\n parsedJson = {\n ...parsedJson,\n model: globalModel\n };\n }\n\n const finalConfig: CachedConfig = {\n parsedJson,\n enabled,\n };\n\n await kv.put(key, JSON.stringify(finalConfig), { expirationTtl: 60 * 10 });\n\n if (!existing) {\n await syncRepoConfig(db, input);\n }\n\n return finalConfig;\n}\n","import type { KvCompat } from './kv';\n\nconst EMAILS_API_URL = 'https://codra.run/api/emails';\n\ntype UpdatesEmailRecord = {\n status: 'subscribed';\n email: string;\n updatedAt: string;\n};\n\nfunction updatesEmailKey(githubUserId: number) {\n return `updates-email:${githubUserId}`;\n}\n\nexport async function getUpdatesEmailPreference(\n kv: KvCompat,\n githubUserId: number,\n) {\n return await kv.get(updatesEmailKey(githubUserId), 'json') as UpdatesEmailRecord | null;\n}\n\nasync function hasUpdatesEmailPreference(\n kv: KvCompat,\n githubUserId: number,\n) {\n return Boolean(await getUpdatesEmailPreference(kv, githubUserId));\n}\n\nexport async function syncUpdatesEmail(\n kv: KvCompat,\n githubUserId: number,\n email: string | null | undefined,\n) {\n if (!email) return false;\n\n if (await hasUpdatesEmailPreference(kv, githubUserId)) return false;\n\n const response = await fetch(EMAILS_API_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ email }),\n });\n\n if (!response.ok) {\n // Need to import logger. If it doesn't exist in the current scope, add it.\n // import { logger } from '../logger'; // Uncomment and adjust path if needed\n console.warn('Failed to sync updates email', { status: response.status, url: response.url }); // Using console.warn as a fallback, replace with logger.warn if imported\n return false;\n }\n\n const record: UpdatesEmailRecord = {\n status: 'subscribed',\n email,\n updatedAt: new Date().toISOString(),\n };\n await kv.put(updatesEmailKey(githubUserId), JSON.stringify(record));\n\n return true;\n}\n","import { randomHex } from '@codraoss/schema/hex';\nimport type { KvCompat } from './kv';\n\nconst OAUTH_STATE_TTL_SECONDS = 60 * 10;\n\nfunction oauthStateKey(state: string) {\n return `oauth-state:${state}`;\n}\n\nexport function parseAllowedUsers(input: string) {\n return new Set(\n input\n .split(',')\n .map((value) => value.trim().toLowerCase())\n .filter(Boolean),\n );\n}\n\nexport async function createOAuthState(kv: KvCompat) {\n const state = randomHex();\n await kv.put(\n oauthStateKey(state),\n JSON.stringify({ createdAt: new Date().toISOString() }),\n { expirationTtl: OAUTH_STATE_TTL_SECONDS },\n );\n return state;\n}\n\nexport async function consumeOAuthState(kv: KvCompat, state: string) {\n const key = oauthStateKey(state);\n const value = await kv.get(key);\n if (!value) {\n return false;\n }\n\n await kv.delete(key);\n return true;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,53 @@
1
+ // src/logger.ts
2
+ import { AsyncLocalStorage } from "async_hooks";
3
+ import { formatLogRecord, setLoggerSink } from "@codraoss/core/logger";
4
+ var storage = new AsyncLocalStorage();
5
+ var Logger = class _Logger {
6
+ constructor(context = {}) {
7
+ this.context = context;
8
+ }
9
+ withContext(newContext) {
10
+ return new _Logger({ ...this.context, ...newContext });
11
+ }
12
+ log(level, message, data) {
13
+ const store = storage.getStore() || {};
14
+ const output = formatLogRecord(level, message, [store, this.context], data);
15
+ if (level === "error") {
16
+ console.error(JSON.stringify(output));
17
+ } else if (level === "warn") {
18
+ console.warn(JSON.stringify(output));
19
+ } else {
20
+ console.log(JSON.stringify(output));
21
+ }
22
+ }
23
+ runWithContext(context, fn) {
24
+ return storage.run({ ...storage.getStore(), ...context }, fn);
25
+ }
26
+ info(message, data) {
27
+ this.log("info", message, data);
28
+ }
29
+ error(message, data) {
30
+ if (data instanceof Error) {
31
+ this.log("error", message, {
32
+ name: data.name,
33
+ message: data.message,
34
+ stack: data.stack
35
+ });
36
+ } else {
37
+ this.log("error", message, data);
38
+ }
39
+ }
40
+ warn(message, data) {
41
+ this.log("warn", message, data);
42
+ }
43
+ debug(message, data) {
44
+ this.log("debug", message, data);
45
+ }
46
+ };
47
+ var logger = new Logger();
48
+ setLoggerSink(logger);
49
+
50
+ export {
51
+ logger
52
+ };
53
+ //# sourceMappingURL=chunk-RMQQ4RZ4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logger.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport { formatLogRecord, setLoggerSink } from '@codraoss/core/logger';\n\n// The request-context half of the logger. Scrubbing and record shaping live in @codraoss/core/logger;\n// this file owns everything platform-bound -- AsyncLocalStorage and the console sink -- so that\n// node:async_hooks never enters the engine package. Importing this module installs it as the sink\n// that @codraoss/core's `logger` facade delegates to (see the bottom of the file).\nconst storage = new AsyncLocalStorage<Record<string, any>>();\n\nclass Logger {\n constructor(private context: Record<string, any> = {}) {}\n\n withContext(newContext: Record<string, any>) {\n return new Logger({ ...this.context, ...newContext });\n }\n\n private log(level: string, message: string, data?: any) {\n const store = storage.getStore() || {};\n const output = formatLogRecord(level, message, [store, this.context], data);\n\n if (level === 'error') {\n console.error(JSON.stringify(output));\n } else if (level === 'warn') {\n console.warn(JSON.stringify(output));\n } else {\n console.log(JSON.stringify(output));\n }\n }\n\n runWithContext<T>(context: Record<string, any>, fn: () => T): T {\n return storage.run({ ...storage.getStore(), ...context }, fn);\n }\n\n info(message: string, data?: any) {\n this.log('info', message, data);\n }\n\n error(message: string, data?: any) {\n if (data instanceof Error) {\n this.log('error', message, {\n name: data.name,\n message: data.message,\n stack: data.stack,\n });\n } else {\n this.log('error', message, data);\n }\n }\n\n warn(message: string, data?: any) {\n this.log('warn', message, data);\n }\n\n debug(message: string, data?: any) {\n this.log('debug', message, data);\n }\n}\n\nexport const logger = new Logger();\n\n// Wired at import scope so engine code logging through @codraoss/core's facade lands here, with request\n// context attached, rather than in core's bare console fallback.\nsetLoggerSink(logger);\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,iBAAiB,qBAAqB;AAM/C,IAAM,UAAU,IAAI,kBAAuC;AAE3D,IAAM,SAAN,MAAM,QAAO;AAAA,EACX,YAAoB,UAA+B,CAAC,GAAG;AAAnC;AAAA,EAAoC;AAAA,EAExD,YAAY,YAAiC;AAC3C,WAAO,IAAI,QAAO,EAAE,GAAG,KAAK,SAAS,GAAG,WAAW,CAAC;AAAA,EACtD;AAAA,EAEQ,IAAI,OAAe,SAAiB,MAAY;AACtD,UAAM,QAAQ,QAAQ,SAAS,KAAK,CAAC;AACrC,UAAM,SAAS,gBAAgB,OAAO,SAAS,CAAC,OAAO,KAAK,OAAO,GAAG,IAAI;AAE1E,QAAI,UAAU,SAAS;AACrB,cAAQ,MAAM,KAAK,UAAU,MAAM,CAAC;AAAA,IACtC,WAAW,UAAU,QAAQ;AAC3B,cAAQ,KAAK,KAAK,UAAU,MAAM,CAAC;AAAA,IACrC,OAAO;AACL,cAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,eAAkB,SAA8B,IAAgB;AAC9D,WAAO,QAAQ,IAAI,EAAE,GAAG,QAAQ,SAAS,GAAG,GAAG,QAAQ,GAAG,EAAE;AAAA,EAC9D;AAAA,EAEA,KAAK,SAAiB,MAAY;AAChC,SAAK,IAAI,QAAQ,SAAS,IAAI;AAAA,EAChC;AAAA,EAEA,MAAM,SAAiB,MAAY;AACjC,QAAI,gBAAgB,OAAO;AACzB,WAAK,IAAI,SAAS,SAAS;AAAA,QACzB,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,OAAO,KAAK;AAAA,MACd,CAAC;AAAA,IACH,OAAO;AACL,WAAK,IAAI,SAAS,SAAS,IAAI;AAAA,IACjC;AAAA,EACF;AAAA,EAEA,KAAK,SAAiB,MAAY;AAChC,SAAK,IAAI,QAAQ,SAAS,IAAI;AAAA,EAChC;AAAA,EAEA,MAAM,SAAiB,MAAY;AACjC,SAAK,IAAI,SAAS,SAAS,IAAI;AAAA,EACjC;AACF;AAEO,IAAM,SAAS,IAAI,OAAO;AAIjC,cAAc,MAAM;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import * as hono from 'hono';
3
3
  import { MiddlewareHandler, Hono, Context } from 'hono';
4
- import { SessionStore, ReviewRuntime, DashboardSessionUser } from '@codraoss/core/ports';
4
+ import { SessionStore, ReviewRuntime, DashboardSessionUser, IdentityProvider } from '@codraoss/core/ports';
5
5
  import { ApiAction } from '@codraoss/schema/api';
6
6
  import * as dbAccounts from '@codraoss/db/accounts';
7
7
  import * as dbJobs from '@codraoss/db/jobs';
@@ -12,6 +12,8 @@ import * as dbRepoConfigs from '@codraoss/db/repo-configs';
12
12
  import * as dbAppSettings from '@codraoss/db/app-settings';
13
13
  import * as dbStats from '@codraoss/db/stats';
14
14
  import * as dbWebhookDeliveries from '@codraoss/db/webhook-deliveries';
15
+ import { DbEnv } from '@codraoss/db/env';
16
+ import { K as KvCompat } from './kv-ByAbDUqn.js';
15
17
 
16
18
  interface RepositoriesPort {
17
19
  accounts: typeof dbAccounts;
@@ -191,4 +193,38 @@ declare const requireSession: hono.MiddlewareHandler<ApiEnv, string, {}, Respons
191
193
 
192
194
  declare const requireCsrfHeader: hono.MiddlewareHandler<ApiEnv, string, {}, Response>;
193
195
 
194
- export { type ApiEnv, type ApiRouterDeps, type ApiRouterOptions, type AuthorizeContext, type AuthorizeResult, type AuthzPort, type ConfigPort, type PlatformPort, type QuotaCheckInput, type QuotaResult, type RepositoriesPort, createApiRouter, requireCsrfHeader, requirePermission, requireQuota, requireSession };
196
+ interface PlatformDeps {
197
+ sessionStore: SessionStore;
198
+ kv: KvCompat;
199
+ db: DbEnv;
200
+ identityProvider?: IdentityProvider;
201
+ enqueueReviewJob: (input: any) => Promise<void>;
202
+ terminateJobWorkflow: (job: {
203
+ id: string;
204
+ workflowInstanceId?: string | null;
205
+ }) => Promise<void>;
206
+ scheduleBestEffortJobMaintenance: (executionContext?: any) => void;
207
+ createReviewRuntime: () => ReviewRuntime;
208
+ getOrFetchRawDiffForCompletedJob: (runtime: ReviewRuntime, job: any, github: any) => Promise<string>;
209
+ logger: PlatformPort['logger'];
210
+ getSecret: (key: string) => Promise<string | null>;
211
+ aiBinding?: any;
212
+ appUrl: string;
213
+ botUsername: string;
214
+ environment: string;
215
+ authCallbackUrl: string;
216
+ githubClientId: string;
217
+ githubClientSecret: string;
218
+ githubAppSlug?: string;
219
+ dashboardAllowedUsers: string;
220
+ appPrivateKey: string;
221
+ githubAppId: string;
222
+ githubAppWebhookSecret: string;
223
+ llmConfigEncryptionKey: string;
224
+ cfApiToken?: string;
225
+ cfAccountId?: string;
226
+ }
227
+
228
+ declare function createSharedApiDeps(p: PlatformDeps): ApiRouterDeps;
229
+
230
+ export { type ApiEnv, type ApiRouterDeps, type ApiRouterOptions, type AuthorizeContext, type AuthorizeResult, type AuthzPort, type ConfigPort, type PlatformDeps, type PlatformPort, type QuotaCheckInput, type QuotaResult, type RepositoriesPort, createApiRouter, createSharedApiDeps, requireCsrfHeader, requirePermission, requireQuota, requireSession };
package/dist/index.js CHANGED
@@ -1,3 +1,17 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-RMQQ4RZ4.js";
4
+ import {
5
+ consumeOAuthState,
6
+ createOAuthState,
7
+ getGlobalConfig,
8
+ getUpdatesEmailPreference,
9
+ invalidateRepoConfigCache,
10
+ loadRepoConfig,
11
+ syncUpdatesEmail,
12
+ updateGlobalConfig
13
+ } from "./chunk-N45ONQ2Q.js";
14
+
1
15
  // src/router.ts
2
16
  import { Hono as Hono9 } from "hono";
3
17
 
@@ -58,7 +72,7 @@ var requireSession = createMiddleware(async (c, next) => {
58
72
  const session = await readSession(c);
59
73
  if (!session) {
60
74
  if (wantsHtml(c.req.raw)) {
61
- return c.redirect("/login");
75
+ return c.redirect("/");
62
76
  }
63
77
  return Response.json({ error: "Unauthorized" }, { status: 401 });
64
78
  }
@@ -80,56 +94,6 @@ var requireCsrfHeader = createMiddleware2(async (c, next) => {
80
94
  await next();
81
95
  });
82
96
 
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
97
  // src/middleware/observability.ts
134
98
  var observability = async (c, next) => {
135
99
  const requestId = c.req.header("x-request-id") || crypto.randomUUID();
@@ -1254,8 +1218,12 @@ function createSettingsRouter() {
1254
1218
  async function serveIndex(c) {
1255
1219
  const assets = c.env.ASSETS;
1256
1220
  if (assets && typeof assets.fetch === "function") {
1257
- return assets.fetch(new Request(new URL("/", c.req.url), c.req.raw));
1221
+ const response = await assets.fetch(new Request(new URL("/", c.req.url), c.req.raw));
1222
+ const newResponse = new Response(response.body, { status: response.status, statusText: response.statusText, headers: response.headers });
1223
+ newResponse.headers.set("Cache-Control", "no-cache");
1224
+ return newResponse;
1258
1225
  }
1226
+ c.header("Cache-Control", "no-cache");
1259
1227
  return c.text("Not Found: Please mount UI static assets handler here.", 404);
1260
1228
  }
1261
1229
  function createApiRouter(options = {}) {
@@ -1264,6 +1232,7 @@ function createApiRouter(options = {}) {
1264
1232
  for (const middleware of options.beforeAuth ?? []) {
1265
1233
  app.use("*", middleware);
1266
1234
  }
1235
+ app.get("/healthz", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
1267
1236
  app.use("/auth/logout", requireSession);
1268
1237
  app.use("/auth/logout", requireCsrfHeader);
1269
1238
  app.route("/auth", createAuthRouter());
@@ -1298,8 +1267,249 @@ function createApiRouter(options = {}) {
1298
1267
  options.routes?.(app);
1299
1268
  return app;
1300
1269
  }
1270
+
1271
+ // src/create-deps.ts
1272
+ import * as dbAccounts from "@codraoss/db/accounts";
1273
+ import * as dbJobs from "@codraoss/db/jobs";
1274
+ import * as dbFileReviews from "@codraoss/db/file-reviews";
1275
+ import * as dbCommentFeedback from "@codraoss/db/comment-feedback";
1276
+ import * as dbModelConfigs from "@codraoss/db/model-configs";
1277
+ import * as dbRepoConfigs from "@codraoss/db/repo-configs";
1278
+ import * as dbAppSettings from "@codraoss/db/app-settings";
1279
+ import * as dbStats from "@codraoss/db/stats";
1280
+ import * as dbWebhookDeliveries from "@codraoss/db/webhook-deliveries";
1281
+ import { GitHubClient, normalizeGitHubWebhook } from "@codraoss/provider-github";
1282
+ import { GitHubIdentityProvider } from "@codraoss/provider-github/oauth";
1283
+ import { extractReviewRequest } from "@codraoss/core";
1284
+ import { verifyGitHubWebhookSignature } from "@codraoss/core/verify";
1285
+ import { listLlmProviderSecrets, upsertDiscoveredModelConfigs, createLlmProvider, updateLlmProvider, getResolvedModelConfig, getLlmProvider } from "@codraoss/db/model-configs";
1286
+ import { encryptLlmApiKey, decryptLlmApiKey, listProviderModels, reviewWithCloudflare, reviewWithGoogle, reviewWithVertex, reviewWithOpenAI, reviewWithAnthropic, ProviderRequestError } from "@codraoss/models";
1287
+ import { buildReviewResponseSchema } from "@codraoss/core/prompts/file-review";
1288
+ function toAppBindingsConfig(p) {
1289
+ return {
1290
+ APP_KV: p.kv,
1291
+ APP_PRIVATE_KEY: p.appPrivateKey,
1292
+ GITHUB_APP_ID: p.githubAppId,
1293
+ BOT_USERNAME: p.botUsername,
1294
+ GITHUB_APP_SLUG: p.githubAppSlug,
1295
+ GITHUB_CLIENT_ID: p.githubClientId,
1296
+ GITHUB_CLIENT_SECRET: p.githubClientSecret,
1297
+ AUTH_CALLBACK_URL: p.authCallbackUrl
1298
+ };
1299
+ }
1300
+ function getSecretStore(p) {
1301
+ return { getSecret: p.getSecret };
1302
+ }
1303
+ function optionalEnv(value) {
1304
+ try {
1305
+ const resolved = value().trim();
1306
+ return resolved.length > 0 ? resolved : void 0;
1307
+ } catch {
1308
+ return void 0;
1309
+ }
1310
+ }
1311
+ var githubIdentity = new GitHubIdentityProvider();
1312
+ function identityProvider(p) {
1313
+ return p.identityProvider ?? githubIdentity;
1314
+ }
1315
+ function createSharedApiDeps(p) {
1316
+ return {
1317
+ repositories: {
1318
+ accounts: dbAccounts,
1319
+ jobs: dbJobs,
1320
+ fileReviews: dbFileReviews,
1321
+ commentFeedback: dbCommentFeedback,
1322
+ modelConfigs: dbModelConfigs,
1323
+ repoConfigs: dbRepoConfigs,
1324
+ appSettings: dbAppSettings,
1325
+ stats: dbStats,
1326
+ webhookDeliveries: dbWebhookDeliveries
1327
+ },
1328
+ gitProvider: {
1329
+ getAppInstallationUrl: async () => await GitHubClient.getAppInstallationUrl(toAppBindingsConfig(p)),
1330
+ listInstallations: async () => await GitHubClient.listInstallations(toAppBindingsConfig(p)),
1331
+ createService: (installationId) => new GitHubClient(toAppBindingsConfig(p), installationId != null ? String(installationId) : void 0)
1332
+ },
1333
+ config: {
1334
+ getGlobalConfig: async () => await getGlobalConfig(p.kv),
1335
+ updateGlobalConfig: async (config) => await updateGlobalConfig(p.kv, config),
1336
+ loadRepoConfig: async (input) => await loadRepoConfig(p.kv, p.db, input),
1337
+ invalidateRepoConfigCache: async (owner, repo) => await invalidateRepoConfigCache(p.kv, owner, repo)
1338
+ },
1339
+ modelRunner: {
1340
+ syncProviderModelCatalog: async () => {
1341
+ const providers = await listLlmProviderSecrets(p.db);
1342
+ const syncErrors = [];
1343
+ await Promise.all(providers.map(async (provider) => {
1344
+ if (!provider.enabled) return;
1345
+ if (provider.apiFormat !== "cloudflare-workers-ai" && !provider.encryptedApiKey) return;
1346
+ try {
1347
+ const apiKey = provider.encryptedApiKey ? await decryptLlmApiKey(getSecretStore(p), provider.encryptedApiKey) : void 0;
1348
+ const modelNames = await listProviderModels({
1349
+ apiFormat: provider.apiFormat,
1350
+ baseUrl: provider.baseUrl,
1351
+ apiKey,
1352
+ cloudflareAccountId: optionalEnv(() => p.cfAccountId || ""),
1353
+ cloudflareApiToken: optionalEnv(() => p.cfApiToken || "")
1354
+ });
1355
+ await upsertDiscoveredModelConfigs(p.db, {
1356
+ providerId: provider.id,
1357
+ providerName: provider.name,
1358
+ apiFormat: provider.apiFormat,
1359
+ modelNames
1360
+ });
1361
+ } catch (error) {
1362
+ syncErrors.push({
1363
+ providerId: provider.id,
1364
+ providerName: provider.name,
1365
+ error: error instanceof Error ? error.message : "Could not refresh provider models."
1366
+ });
1367
+ }
1368
+ }));
1369
+ return syncErrors;
1370
+ },
1371
+ testConnection: async (modelId) => {
1372
+ const config = await getResolvedModelConfig(p.db, modelId);
1373
+ if (!config) throw { isNotFoundError: true };
1374
+ if (!config.providerEnabled) throw { isDisabledError: true, message: "Provider is disabled." };
1375
+ try {
1376
+ const input = {
1377
+ systemPrompt: "You are validating connectivity. Return only the JSON object.",
1378
+ userPrompt: 'Return an empty review: no findings, overall_correctness "patch is correct".',
1379
+ responseSchema: buildReviewResponseSchema(1)
1380
+ };
1381
+ let response;
1382
+ if (config.apiFormat === "cloudflare-workers-ai") {
1383
+ response = await reviewWithCloudflare(p.aiBinding, config.modelName, input, void 0, config.providerName);
1384
+ } else {
1385
+ if (!config.encryptedApiKey) {
1386
+ throw { isMissingKeyError: true, message: `Provider ${config.providerName} does not have a saved API key.` };
1387
+ }
1388
+ const apiKey = await decryptLlmApiKey(getSecretStore(p), config.encryptedApiKey);
1389
+ switch (config.apiFormat) {
1390
+ case "gemini":
1391
+ response = await reviewWithGoogle({ apiKey, baseUrl: config.baseUrl, providerName: config.providerName, timeoutMs: 15e3 }, config.modelName, input);
1392
+ break;
1393
+ case "vertex":
1394
+ response = await reviewWithVertex({ apiKey, baseUrl: config.baseUrl, providerName: config.providerName }, config.modelName, input);
1395
+ break;
1396
+ case "openai":
1397
+ response = await reviewWithOpenAI({ apiKey, baseUrl: config.baseUrl || "https://api.openai.com/v1", providerName: config.providerName }, config.modelName, input);
1398
+ break;
1399
+ case "anthropic":
1400
+ response = await reviewWithAnthropic({ apiKey, baseUrl: config.baseUrl, providerName: config.providerName }, config.modelName, input);
1401
+ break;
1402
+ default:
1403
+ throw new Error(`Unsupported API format: ${config.apiFormat}`);
1404
+ }
1405
+ }
1406
+ return {
1407
+ ok: true,
1408
+ modelUsed: response.modelUsed,
1409
+ provider: response.provider,
1410
+ inputTokens: response.inputTokens,
1411
+ outputTokens: response.outputTokens,
1412
+ ...response.degraded === "schema-dropped" ? { degraded: response.degraded, warning: "Connected, but this endpoint rejected the response grammar. Reviews will run without constrained decoding." } : {}
1413
+ };
1414
+ } catch (error) {
1415
+ if (error instanceof ProviderRequestError) {
1416
+ throw { status: error.status >= 500 ? 502 : error.status, message: error.message, originalError: error };
1417
+ }
1418
+ throw error;
1419
+ }
1420
+ },
1421
+ createProviderWithSecret: async (input) => {
1422
+ let encryptedApiKey;
1423
+ try {
1424
+ encryptedApiKey = input.apiFormat === "cloudflare-workers-ai" ? null : input.apiKey ? await encryptLlmApiKey(getSecretStore(p), input.apiKey.trim()) : null;
1425
+ } catch (error) {
1426
+ if (error instanceof Error && error.message.includes("LLM_CONFIG_ENCRYPTION_KEY")) {
1427
+ throw { isEncryptionConfigError: true, message: error.message };
1428
+ }
1429
+ throw error;
1430
+ }
1431
+ if (input.enabled && input.apiFormat !== "cloudflare-workers-ai" && !encryptedApiKey) {
1432
+ throw { isKeyRequiredError: true };
1433
+ }
1434
+ try {
1435
+ return await createLlmProvider(p.db, {
1436
+ name: input.name,
1437
+ apiFormat: input.apiFormat,
1438
+ baseUrl: input.apiFormat === "cloudflare-workers-ai" ? null : input.baseUrl ? input.baseUrl.replace(/\/+$/, "") : null,
1439
+ // basic normalization
1440
+ encryptedApiKey,
1441
+ enabled: input.enabled
1442
+ });
1443
+ } catch (error) {
1444
+ if (error?.code === "23505") throw { isUniqueNameError: true };
1445
+ throw error;
1446
+ }
1447
+ },
1448
+ updateProviderWithSecret: async (id, input) => {
1449
+ const existing = await getLlmProvider(p.db, id);
1450
+ if (!existing) return null;
1451
+ let encryptedApiKey;
1452
+ try {
1453
+ if (input.apiFormat === "cloudflare-workers-ai") {
1454
+ encryptedApiKey = null;
1455
+ } else if (input.clearApiKey) {
1456
+ encryptedApiKey = null;
1457
+ } else if (input.apiKey) {
1458
+ encryptedApiKey = await encryptLlmApiKey(getSecretStore(p), input.apiKey.trim());
1459
+ } else {
1460
+ encryptedApiKey = void 0;
1461
+ }
1462
+ } catch (error) {
1463
+ if (error instanceof Error && error.message.includes("LLM_CONFIG_ENCRYPTION_KEY")) {
1464
+ throw { isEncryptionConfigError: true, message: error.message };
1465
+ }
1466
+ throw error;
1467
+ }
1468
+ const effectiveEncryptedApiKey = encryptedApiKey !== void 0 ? encryptedApiKey : existing.encryptedApiKey;
1469
+ if (input.enabled && input.apiFormat !== "cloudflare-workers-ai" && !effectiveEncryptedApiKey) {
1470
+ throw { isKeyRequiredError: true };
1471
+ }
1472
+ try {
1473
+ return await updateLlmProvider(p.db, id, {
1474
+ name: input.name,
1475
+ apiFormat: input.apiFormat,
1476
+ baseUrl: input.apiFormat === "cloudflare-workers-ai" ? null : input.baseUrl ? input.baseUrl.replace(/\/+$/, "") : null,
1477
+ ...encryptedApiKey !== void 0 ? { encryptedApiKey } : {},
1478
+ enabled: input.enabled
1479
+ });
1480
+ } catch (error) {
1481
+ if (error?.code === "23505") throw { isUniqueNameError: true };
1482
+ throw error;
1483
+ }
1484
+ }
1485
+ },
1486
+ sessionStore: p.sessionStore,
1487
+ platform: {
1488
+ scheduleBestEffortJobMaintenance: p.scheduleBestEffortJobMaintenance,
1489
+ createReviewRuntime: p.createReviewRuntime,
1490
+ getUpdatesEmailPreference: async (githubUserId) => await getUpdatesEmailPreference(p.kv, githubUserId),
1491
+ syncUpdatesEmail: async (githubUserId, email) => await syncUpdatesEmail(p.kv, githubUserId, email),
1492
+ terminateJobWorkflow: p.terminateJobWorkflow,
1493
+ enqueueReviewJob: p.enqueueReviewJob,
1494
+ getOrFetchRawDiffForCompletedJob: p.getOrFetchRawDiffForCompletedJob,
1495
+ logger: p.logger
1496
+ },
1497
+ authProvider: {
1498
+ createOAuthState: async () => await createOAuthState(p.kv),
1499
+ consumeOAuthState: async (state) => await consumeOAuthState(p.kv, state),
1500
+ beginAuthorization: async (callbackUrl, state) => await identityProvider(p).beginAuthorization(callbackUrl, state, toAppBindingsConfig(p)),
1501
+ completeAuthorization: async (code, state, expectedState) => await identityProvider(p).completeAuthorization(code, state, expectedState, toAppBindingsConfig(p))
1502
+ },
1503
+ webhook: {
1504
+ verifySignature: async (signature, body) => await verifyGitHubWebhookSignature(p.githubAppWebhookSecret, signature, body),
1505
+ normalizePayload: (eventName, payload) => normalizeGitHubWebhook(eventName, payload),
1506
+ extractReviewRequest: (input) => extractReviewRequest(input)
1507
+ }
1508
+ };
1509
+ }
1301
1510
  export {
1302
1511
  createApiRouter,
1512
+ createSharedApiDeps,
1303
1513
  requireCsrfHeader,
1304
1514
  requirePermission,
1305
1515
  requireQuota,