@codraoss/api 0.9.5 → 0.9.12
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/chunk-N45ONQ2Q.js +145 -0
- package/dist/chunk-N45ONQ2Q.js.map +1 -0
- package/dist/chunk-RMQQ4RZ4.js +53 -0
- package/dist/chunk-RMQQ4RZ4.js.map +1 -0
- package/dist/index.d.ts +38 -2
- package/dist/index.js +268 -53
- package/dist/index.js.map +1 -1
- package/dist/kv-ByAbDUqn.d.ts +10 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +7 -0
- package/dist/logger.js.map +1 -0
- package/dist/platform/index.d.ts +30 -0
- package/dist/platform/index.js +23 -0
- package/dist/platform/index.js.map +1 -0
- package/package.json +22 -6
|
@@ -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
|
-
|
|
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("/
|
|
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();
|
|
@@ -584,6 +548,11 @@ async function terminateJobWorkflow(c, job) {
|
|
|
584
548
|
function jobEtag(input) {
|
|
585
549
|
return `"job-${input.id}-${input.status}-${input.fileCount}-${input.commentCount}-${new Date(input.updatedAt).getTime()}"`;
|
|
586
550
|
}
|
|
551
|
+
function etagMatches(header, etag) {
|
|
552
|
+
if (!header) return false;
|
|
553
|
+
const bare = etag.replace(/^W\//, "");
|
|
554
|
+
return header.split(",").some((candidate) => candidate.trim().replace(/^W\//, "") === bare);
|
|
555
|
+
}
|
|
587
556
|
function getExecutionContext(c) {
|
|
588
557
|
try {
|
|
589
558
|
return c.executionCtx;
|
|
@@ -612,7 +581,7 @@ function createJobsRouter() {
|
|
|
612
581
|
}
|
|
613
582
|
const etag = jobEtag(job);
|
|
614
583
|
const lastModified = new Date(job.updatedAt).toUTCString();
|
|
615
|
-
if (c.req.header("if-none-match")
|
|
584
|
+
if (etagMatches(c.req.header("if-none-match"), etag)) {
|
|
616
585
|
return new Response(null, {
|
|
617
586
|
status: 304,
|
|
618
587
|
headers: {
|
|
@@ -1249,8 +1218,12 @@ function createSettingsRouter() {
|
|
|
1249
1218
|
async function serveIndex(c) {
|
|
1250
1219
|
const assets = c.env.ASSETS;
|
|
1251
1220
|
if (assets && typeof assets.fetch === "function") {
|
|
1252
|
-
|
|
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;
|
|
1253
1225
|
}
|
|
1226
|
+
c.header("Cache-Control", "no-cache");
|
|
1254
1227
|
return c.text("Not Found: Please mount UI static assets handler here.", 404);
|
|
1255
1228
|
}
|
|
1256
1229
|
function createApiRouter(options = {}) {
|
|
@@ -1259,6 +1232,7 @@ function createApiRouter(options = {}) {
|
|
|
1259
1232
|
for (const middleware of options.beforeAuth ?? []) {
|
|
1260
1233
|
app.use("*", middleware);
|
|
1261
1234
|
}
|
|
1235
|
+
app.get("/healthz", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
1262
1236
|
app.use("/auth/logout", requireSession);
|
|
1263
1237
|
app.use("/auth/logout", requireCsrfHeader);
|
|
1264
1238
|
app.route("/auth", createAuthRouter());
|
|
@@ -1293,8 +1267,249 @@ function createApiRouter(options = {}) {
|
|
|
1293
1267
|
options.routes?.(app);
|
|
1294
1268
|
return app;
|
|
1295
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
|
+
}
|
|
1296
1510
|
export {
|
|
1297
1511
|
createApiRouter,
|
|
1512
|
+
createSharedApiDeps,
|
|
1298
1513
|
requireCsrfHeader,
|
|
1299
1514
|
requirePermission,
|
|
1300
1515
|
requireQuota,
|