vibe-roast 0.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +418 -0
- package/assests/README.md +43 -0
- package/assests/badges/.gitkeep +0 -0
- package/assests/badges/01-builder/builder-badge.png +0 -0
- package/assests/badges/01-builder/builder-badge.svg +4 -0
- package/assests/badges/02-debugger/debugger-badge.png +0 -0
- package/assests/badges/02-debugger/debugger-badge.svg +4 -0
- package/assests/badges/03-architect/architect-badge.png +0 -0
- package/assests/badges/03-architect/architect-badge.svg +4 -0
- package/assests/badges/04-codex-believer/codex-believer-badge.png +0 -0
- package/assests/badges/04-codex-believer/codex-believer-badge.svg +4 -0
- package/assests/badges/05-prompt-priest/prompt-priest-badge.png +0 -0
- package/assests/badges/05-prompt-priest/prompt-priest-badge.svg +4 -0
- package/assests/badges/06-tab-hoarder/tab-hoarder-badge.png +0 -0
- package/assests/badges/06-tab-hoarder/tab-hoarder-badge.svg +4 -0
- package/assests/badges/07-context-maxxer/context-maxxer-badge.png +0 -0
- package/assests/badges/07-context-maxxer/context-maxxer-badge.svg +4 -0
- package/assests/badges/08-yolo-shipper/yolo-shipper-badge.png +0 -0
- package/assests/badges/08-yolo-shipper/yolo-shipper-badge.svg +4 -0
- package/assests/banners/.gitkeep +0 -0
- package/assests/banners/home-hero.png +0 -0
- package/assests/banners/home-hero.svg +3 -0
- package/assests/banners/result-header.png +0 -0
- package/assests/banners/result-header.svg +3 -0
- package/assests/banners/share-strip.png +0 -0
- package/assests/banners/share-strip.svg +3 -0
- package/assests/characters/01-builder/builder-card.sbti-archive.svg +5 -0
- package/assests/characters/01-builder/builder-card.svg +3 -0
- package/assests/characters/01-builder/builder-character.sbti-archive.svg +3 -0
- package/assests/characters/01-builder/builder-character.svg +3 -0
- package/assests/characters/01-builder/builder-figure.png +0 -0
- package/assests/characters/01-builder/builder-mascot.png +0 -0
- package/assests/characters/02-debugger/debugger-card.sbti-archive.svg +5 -0
- package/assests/characters/02-debugger/debugger-card.svg +3 -0
- package/assests/characters/02-debugger/debugger-character.sbti-archive.svg +3 -0
- package/assests/characters/02-debugger/debugger-character.svg +3 -0
- package/assests/characters/02-debugger/debugger-figure.png +0 -0
- package/assests/characters/02-debugger/debugger-mascot.png +0 -0
- package/assests/characters/03-architect/architect-card.sbti-archive.svg +5 -0
- package/assests/characters/03-architect/architect-card.svg +3 -0
- package/assests/characters/03-architect/architect-character.sbti-archive.svg +3 -0
- package/assests/characters/03-architect/architect-character.svg +3 -0
- package/assests/characters/03-architect/architect-figure.png +0 -0
- package/assests/characters/03-architect/architect-mascot.png +0 -0
- package/assests/characters/04-codex-believer/codex-believer-card.sbti-archive.svg +5 -0
- package/assests/characters/04-codex-believer/codex-believer-card.svg +3 -0
- package/assests/characters/04-codex-believer/codex-believer-character.sbti-archive.svg +3 -0
- package/assests/characters/04-codex-believer/codex-believer-character.svg +3 -0
- package/assests/characters/04-codex-believer/codex-believer-figure.png +0 -0
- package/assests/characters/04-codex-believer/codex-believer-mascot.png +0 -0
- package/assests/characters/05-prompt-priest/prompt-priest-card.sbti-archive.svg +5 -0
- package/assests/characters/05-prompt-priest/prompt-priest-card.svg +3 -0
- package/assests/characters/05-prompt-priest/prompt-priest-character.sbti-archive.svg +3 -0
- package/assests/characters/05-prompt-priest/prompt-priest-character.svg +3 -0
- package/assests/characters/05-prompt-priest/prompt-priest-figure.png +0 -0
- package/assests/characters/05-prompt-priest/prompt-priest-mascot.png +0 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-card.sbti-archive.svg +5 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-card.svg +3 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-character.sbti-archive.svg +3 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-character.svg +3 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-figure.png +0 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-mascot.png +0 -0
- package/assests/characters/07-context-maxxer/context-maxxer-card.sbti-archive.svg +5 -0
- package/assests/characters/07-context-maxxer/context-maxxer-card.svg +3 -0
- package/assests/characters/07-context-maxxer/context-maxxer-character.sbti-archive.svg +3 -0
- package/assests/characters/07-context-maxxer/context-maxxer-character.svg +3 -0
- package/assests/characters/07-context-maxxer/context-maxxer-figure.png +0 -0
- package/assests/characters/07-context-maxxer/context-maxxer-mascot.png +0 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-card.sbti-archive.svg +5 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-card.svg +3 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-character.sbti-archive.svg +3 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-character.svg +3 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-figure.png +0 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-mascot.png +0 -0
- package/assests/characters-vibe-types/aosf-strategy-shipper/strategy-shipper-figure.png +0 -0
- package/assests/characters-vibe-types/aosx-agent-believer/agent-believer-figure.png +0 -0
- package/assests/characters-vibe-types/aovf-systems-architect/systems-architect-figure.png +0 -0
- package/assests/characters-vibe-types/aovx-context-cartographer/context-cartographer-figure.png +0 -0
- package/assests/characters-vibe-types/apsf-diagram-sprinter/diagram-sprinter-figure.png +0 -0
- package/assests/characters-vibe-types/apsx-infinite-planner/infinite-planner-figure.png +0 -0
- package/assests/characters-vibe-types/apvf-architect/architect-figure.png +0 -0
- package/assests/characters-vibe-types/apvx-prompt-priest/prompt-priest-figure.png +0 -0
- package/assests/characters-vibe-types/mosf-yolo-shipper/yolo-shipper-figure.png +0 -0
- package/assests/characters-vibe-types/mosx-agent-commander/agent-commander-figure.png +0 -0
- package/assests/characters-vibe-types/movf-agent-engineer/agent-engineer-figure.png +0 -0
- package/assests/characters-vibe-types/movx-systems-wrangler/systems-wrangler-figure.png +0 -0
- package/assests/characters-vibe-types/mpsf-builder/builder-figure.png +0 -0
- package/assests/characters-vibe-types/mpsx-tab-hoarder/tab-hoarder-figure.png +0 -0
- package/assests/characters-vibe-types/mpvf-debugger/debugger-figure.png +0 -0
- package/assests/characters-vibe-types/mpvx-context-maxxer/context-maxxer-figure.png +0 -0
- package/assests/live-report.html +1403 -0
- package/assests/notes/live-inspect-summary.json +3487 -0
- package/assests/scripts/compose-banners.py +148 -0
- package/assests/scripts/live-report-boot.js +2 -0
- package/assests/scripts/profiles.json +104 -0
- package/assests/scripts/qa-faces.mjs +72 -0
- package/assests/scripts/roast-extras.js +619 -0
- package/assests/scripts/score-engine.js +62 -0
- package/assests/scripts/split-badges.py +49 -0
- package/assests/scripts/split-reference.py +38 -0
- package/assests/scripts/sync-banners.mjs +45 -0
- package/assests/scripts/sync-cards.mjs +50 -0
- package/assests/scripts/sync-raster.mjs +118 -0
- package/assests/scripts/vibe-types.json +22 -0
- package/assests/source/design-system.md +50 -0
- package/assests/source/prompts/01-builder.md +19 -0
- package/assests/source/prompts/02-debugger.md +17 -0
- package/assests/source/prompts/03-architect.md +16 -0
- package/assests/source/prompts/04-codex-believer.md +13 -0
- package/assests/source/prompts/05-prompt-priest.md +13 -0
- package/assests/source/prompts/06-tab-hoarder.md +13 -0
- package/assests/source/prompts/07-context-maxxer.md +13 -0
- package/assests/source/prompts/08-yolo-shipper.md +13 -0
- package/assests/source/references/sbti-style-reference-board.png +0 -0
- package/assests/source/references/vibe-badges-v6-board.png +0 -0
- package/assests/source/references/vibe-mascots-v4-reference.png +0 -0
- package/assests/source/references/vibe-profiles-v5-board-a.png +0 -0
- package/assests/source/references/vibe-profiles-v5-board-b.png +0 -0
- package/assests/source/vibe-types-visual-brief.md +38 -0
- package/bin/hook.js +121 -0
- package/bin/vibe-roast.js +98 -0
- package/bin/vibe-wrapper.js +4 -0
- package/dashboard/dist/assets/index-DlBycTcO.css +1 -0
- package/dashboard/dist/assets/index-t8m64Hm9.js +168 -0
- package/dashboard/dist/index.html +32 -0
- package/package.json +58 -0
- package/src/extract/environment.js +160 -0
- package/src/extract/phrase-stats.js +684 -0
- package/src/extract/prompt-analysis.js +368 -0
- package/src/extract/text.js +30 -0
- package/src/hooks/install.js +184 -0
- package/src/inspect.js +261 -0
- package/src/lib/activity-metrics.js +238 -0
- package/src/lib/agent-score.js +187 -0
- package/src/lib/ai-roast.js +446 -0
- package/src/lib/dates.js +43 -0
- package/src/lib/hashtags.js +150 -0
- package/src/lib/jsonl.js +48 -0
- package/src/lib/load-env.js +59 -0
- package/src/lib/roast-evidence.js +186 -0
- package/src/lib/roast-snapshot.js +58 -0
- package/src/server.js +644 -0
- package/src/sources/aider.js +150 -0
- package/src/sources/amazonq.js +113 -0
- package/src/sources/antigravity.js +147 -0
- package/src/sources/claude.js +262 -0
- package/src/sources/cline.js +11 -0
- package/src/sources/codex.js +222 -0
- package/src/sources/common.js +133 -0
- package/src/sources/continue.js +67 -0
- package/src/sources/copilot.js +170 -0
- package/src/sources/cursor.js +238 -0
- package/src/sources/gemini.js +75 -0
- package/src/sources/index.js +61 -0
- package/src/sources/roo.js +11 -0
- package/src/sources/token-tracker.js +171 -0
- package/src/sources/vibe-tracker.js +79 -0
- package/src/sources/vscode-tasks.js +142 -0
- package/src/sources/windsurf.js +109 -0
- package/worker/README.md +96 -0
- package/worker/package-lock.json +1547 -0
- package/worker/package.json +20 -0
- package/worker/src/index.mjs +694 -0
- package/worker/wrangler.jsonc +40 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
const FLOW_TTL_MS = 10 * 60 * 1000;
|
|
2
|
+
const TICKET_TTL_MS = 2 * 60 * 1000;
|
|
3
|
+
const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
4
|
+
const MAX_EXCHANGE_BODY = 16 * 1024;
|
|
5
|
+
const MAX_AI_BODY = 64 * 1024;
|
|
6
|
+
const API_VERSION = "2026-03-10";
|
|
7
|
+
const FREE_AI_MODEL = "@cf/qwen/qwen3-30b-a3b-fp8";
|
|
8
|
+
const DEFAULT_FREE_AI_DAILY_CALLS = 9;
|
|
9
|
+
|
|
10
|
+
function json(payload, status = 200) {
|
|
11
|
+
return new Response(JSON.stringify(payload), {
|
|
12
|
+
status,
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
15
|
+
"Cache-Control": "no-store",
|
|
16
|
+
"X-Content-Type-Options": "nosniff",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function redirect(location) {
|
|
22
|
+
return new Response(null, {
|
|
23
|
+
status: 302,
|
|
24
|
+
headers: {
|
|
25
|
+
Location: location,
|
|
26
|
+
"Cache-Control": "no-store",
|
|
27
|
+
"Referrer-Policy": "no-referrer",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function base64Url(value) {
|
|
33
|
+
const bytes = value instanceof Uint8Array ? value : new TextEncoder().encode(String(value));
|
|
34
|
+
let binary = "";
|
|
35
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
36
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function randomToken(size = 32) {
|
|
40
|
+
const bytes = new Uint8Array(size);
|
|
41
|
+
crypto.getRandomValues(bytes);
|
|
42
|
+
return base64Url(bytes);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decodeBase64Url(value) {
|
|
46
|
+
const normalized = String(value).replace(/-/g, "+").replace(/_/g, "/");
|
|
47
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
48
|
+
const binary = atob(padded);
|
|
49
|
+
return new Uint8Array([...binary].map((character) => character.charCodeAt(0)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function sha256(value) {
|
|
53
|
+
return base64Url(new Uint8Array(
|
|
54
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)),
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isLoopbackCallback(value) {
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(value);
|
|
61
|
+
return url.protocol === "http:"
|
|
62
|
+
&& ["localhost", "127.0.0.1", "::1"].includes(url.hostname)
|
|
63
|
+
&& url.pathname === "/api/auth/github/callback"
|
|
64
|
+
&& !url.username
|
|
65
|
+
&& !url.password;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function brokerPublicUrl(request, env) {
|
|
72
|
+
const configured = String(env.AUTH_BROKER_PUBLIC_URL || "").replace(/\/+$/, "");
|
|
73
|
+
const url = new URL(configured || request.url);
|
|
74
|
+
return url.origin;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertConfig(env) {
|
|
78
|
+
const missing = [
|
|
79
|
+
"GITHUB_CLIENT_ID",
|
|
80
|
+
"GITHUB_CLIENT_SECRET",
|
|
81
|
+
"AUTH_BROKER_SESSION_SECRET",
|
|
82
|
+
].filter((key) => !env[key]);
|
|
83
|
+
if (missing.length) {
|
|
84
|
+
const error = new Error(`Missing Worker configuration: ${missing.join(", ")}`);
|
|
85
|
+
error.status = 503;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stateStub(env) {
|
|
91
|
+
return env.OAUTH_STATE.get(env.OAUTH_STATE.idFromName("vibe-roast-oauth"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function stateRequest(env, pathname, payload) {
|
|
95
|
+
const response = await stateStub(env).fetch(`https://oauth-state.internal${pathname}`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify(payload),
|
|
99
|
+
});
|
|
100
|
+
const body = await response.json().catch(() => ({}));
|
|
101
|
+
if (!response.ok) throw new Error(body.error || "OAuth state storage failed");
|
|
102
|
+
return body.value ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function signedSession(secret, user, expiresAt) {
|
|
106
|
+
const header = base64Url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
107
|
+
const payload = base64Url(JSON.stringify({
|
|
108
|
+
sub: user.login,
|
|
109
|
+
avatarUrl: user.avatarUrl,
|
|
110
|
+
profileUrl: user.profileUrl,
|
|
111
|
+
iat: Math.floor(Date.now() / 1000),
|
|
112
|
+
exp: Math.floor(expiresAt / 1000),
|
|
113
|
+
}));
|
|
114
|
+
const key = await crypto.subtle.importKey(
|
|
115
|
+
"raw",
|
|
116
|
+
new TextEncoder().encode(secret),
|
|
117
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
118
|
+
false,
|
|
119
|
+
["sign"],
|
|
120
|
+
);
|
|
121
|
+
const signature = await crypto.subtle.sign(
|
|
122
|
+
"HMAC",
|
|
123
|
+
key,
|
|
124
|
+
new TextEncoder().encode(`${header}.${payload}`),
|
|
125
|
+
);
|
|
126
|
+
return `${header}.${payload}.${base64Url(new Uint8Array(signature))}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function verifySignedSession(secret, token) {
|
|
130
|
+
const [encodedHeader, encodedPayload, encodedSignature, ...extra] = String(token || "").split(".");
|
|
131
|
+
if (!encodedHeader || !encodedPayload || !encodedSignature || extra.length) {
|
|
132
|
+
throw Object.assign(new Error("A valid GitHub session is required"), { status: 401 });
|
|
133
|
+
}
|
|
134
|
+
let header;
|
|
135
|
+
let payload;
|
|
136
|
+
try {
|
|
137
|
+
header = JSON.parse(new TextDecoder().decode(decodeBase64Url(encodedHeader)));
|
|
138
|
+
payload = JSON.parse(new TextDecoder().decode(decodeBase64Url(encodedPayload)));
|
|
139
|
+
} catch {
|
|
140
|
+
throw Object.assign(new Error("A valid GitHub session is required"), { status: 401 });
|
|
141
|
+
}
|
|
142
|
+
if (header?.alg !== "HS256" || header?.typ !== "JWT" || !payload?.sub) {
|
|
143
|
+
throw Object.assign(new Error("A valid GitHub session is required"), { status: 401 });
|
|
144
|
+
}
|
|
145
|
+
const key = await crypto.subtle.importKey(
|
|
146
|
+
"raw",
|
|
147
|
+
new TextEncoder().encode(secret),
|
|
148
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
149
|
+
false,
|
|
150
|
+
["verify"],
|
|
151
|
+
);
|
|
152
|
+
const valid = await crypto.subtle.verify(
|
|
153
|
+
"HMAC",
|
|
154
|
+
key,
|
|
155
|
+
decodeBase64Url(encodedSignature),
|
|
156
|
+
new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`),
|
|
157
|
+
);
|
|
158
|
+
if (!valid || !Number.isFinite(Number(payload.exp)) || Number(payload.exp) <= Math.floor(Date.now() / 1000)) {
|
|
159
|
+
throw Object.assign(new Error("The GitHub session is invalid or expired"), { status: 401 });
|
|
160
|
+
}
|
|
161
|
+
return payload;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handleStart(request, env) {
|
|
165
|
+
assertConfig(env);
|
|
166
|
+
const requestUrl = new URL(request.url);
|
|
167
|
+
const callback = String(requestUrl.searchParams.get("callback") || "");
|
|
168
|
+
const localState = String(requestUrl.searchParams.get("state") || "");
|
|
169
|
+
if (!isLoopbackCallback(callback) || !localState || localState.length > 512) {
|
|
170
|
+
return json({ error: "A valid loopback callback and state are required" }, 400);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const brokerState = randomToken();
|
|
174
|
+
const verifier = randomToken(48);
|
|
175
|
+
await stateRequest(env, "/flow/put", {
|
|
176
|
+
key: brokerState,
|
|
177
|
+
value: {
|
|
178
|
+
callback,
|
|
179
|
+
localState,
|
|
180
|
+
verifier,
|
|
181
|
+
expiresAt: Date.now() + FLOW_TTL_MS,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const publicUrl = brokerPublicUrl(request, env);
|
|
186
|
+
const authorizationUrl = new URL("https://github.com/login/oauth/authorize");
|
|
187
|
+
authorizationUrl.searchParams.set("client_id", env.GITHUB_CLIENT_ID);
|
|
188
|
+
authorizationUrl.searchParams.set("redirect_uri", `${publicUrl}/oauth/github/callback`);
|
|
189
|
+
authorizationUrl.searchParams.set("state", brokerState);
|
|
190
|
+
authorizationUrl.searchParams.set("code_challenge", await sha256(verifier));
|
|
191
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
192
|
+
return redirect(authorizationUrl.toString());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function exchangeGithubCode(request, env, flow, code) {
|
|
196
|
+
const publicUrl = brokerPublicUrl(request, env);
|
|
197
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
Accept: "application/json",
|
|
201
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
202
|
+
},
|
|
203
|
+
body: new URLSearchParams({
|
|
204
|
+
client_id: env.GITHUB_CLIENT_ID,
|
|
205
|
+
client_secret: env.GITHUB_CLIENT_SECRET,
|
|
206
|
+
code,
|
|
207
|
+
redirect_uri: `${publicUrl}/oauth/github/callback`,
|
|
208
|
+
code_verifier: flow.verifier,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
const payload = await response.json().catch(() => ({}));
|
|
212
|
+
if (!response.ok || !payload.access_token) {
|
|
213
|
+
const error = new Error(payload.error_description || payload.error || "GitHub token exchange failed");
|
|
214
|
+
error.status = 502;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
return payload.access_token;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function fetchGithubUser(accessToken) {
|
|
221
|
+
const response = await fetch("https://api.github.com/user", {
|
|
222
|
+
headers: {
|
|
223
|
+
Accept: "application/vnd.github+json",
|
|
224
|
+
Authorization: `Bearer ${accessToken}`,
|
|
225
|
+
"User-Agent": "vibe-roast-auth",
|
|
226
|
+
"X-GitHub-Api-Version": API_VERSION,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
const payload = await response.json().catch(() => ({}));
|
|
230
|
+
if (!response.ok || !payload.login) {
|
|
231
|
+
const error = new Error(payload.message || "GitHub returned no user identity");
|
|
232
|
+
error.status = 502;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
login: payload.login,
|
|
237
|
+
avatarUrl: payload.avatar_url,
|
|
238
|
+
profileUrl: payload.html_url,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function handleCallback(request, env) {
|
|
243
|
+
assertConfig(env);
|
|
244
|
+
const requestUrl = new URL(request.url);
|
|
245
|
+
const brokerState = String(requestUrl.searchParams.get("state") || "");
|
|
246
|
+
const flow = await stateRequest(env, "/flow/take", { key: brokerState });
|
|
247
|
+
if (!flow || flow.expiresAt <= Date.now()) {
|
|
248
|
+
return json({ error: "GitHub authorization expired" }, 400);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (requestUrl.searchParams.get("error")) {
|
|
252
|
+
const localUrl = new URL(flow.callback);
|
|
253
|
+
localUrl.searchParams.set("state", flow.localState);
|
|
254
|
+
localUrl.searchParams.set("error", requestUrl.searchParams.get("error"));
|
|
255
|
+
localUrl.searchParams.set(
|
|
256
|
+
"error_description",
|
|
257
|
+
requestUrl.searchParams.get("error_description") || "GitHub authorization was cancelled",
|
|
258
|
+
);
|
|
259
|
+
return redirect(localUrl.toString());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const accessToken = await exchangeGithubCode(
|
|
263
|
+
request,
|
|
264
|
+
env,
|
|
265
|
+
flow,
|
|
266
|
+
String(requestUrl.searchParams.get("code") || ""),
|
|
267
|
+
);
|
|
268
|
+
const user = await fetchGithubUser(accessToken);
|
|
269
|
+
const sessionExpiresAt = Date.now() + SESSION_TTL_MS;
|
|
270
|
+
const ticket = randomToken();
|
|
271
|
+
await stateRequest(env, "/ticket/put", {
|
|
272
|
+
key: ticket,
|
|
273
|
+
value: {
|
|
274
|
+
callback: flow.callback,
|
|
275
|
+
localState: flow.localState,
|
|
276
|
+
user,
|
|
277
|
+
sessionToken: await signedSession(env.AUTH_BROKER_SESSION_SECRET, user, sessionExpiresAt),
|
|
278
|
+
sessionExpiresAt,
|
|
279
|
+
expiresAt: Date.now() + TICKET_TTL_MS,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const localUrl = new URL(flow.callback);
|
|
284
|
+
localUrl.searchParams.set("state", flow.localState);
|
|
285
|
+
localUrl.searchParams.set("ticket", ticket);
|
|
286
|
+
return redirect(localUrl.toString());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function readSmallJson(request, maxBytes = MAX_EXCHANGE_BODY) {
|
|
290
|
+
const contentLength = Number(request.headers.get("content-length") || 0);
|
|
291
|
+
if (contentLength > maxBytes) {
|
|
292
|
+
const error = new Error("Request body is too large");
|
|
293
|
+
error.status = 413;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
const text = await request.text();
|
|
297
|
+
if (text.length > maxBytes) {
|
|
298
|
+
const error = new Error("Request body is too large");
|
|
299
|
+
error.status = 413;
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
return text ? JSON.parse(text) : {};
|
|
304
|
+
} catch {
|
|
305
|
+
const error = new Error("Invalid JSON body");
|
|
306
|
+
error.status = 400;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function bearerToken(request) {
|
|
312
|
+
const match = String(request.headers.get("authorization") || "").match(/^Bearer\s+(.+)$/i);
|
|
313
|
+
return match?.[1] || "";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function freeAiDailyCalls(env) {
|
|
317
|
+
const configured = Number(env.FREE_AI_DAILY_CALLS || DEFAULT_FREE_AI_DAILY_CALLS);
|
|
318
|
+
return Number.isInteger(configured) && configured > 0
|
|
319
|
+
? Math.min(configured, 100)
|
|
320
|
+
: DEFAULT_FREE_AI_DAILY_CALLS;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function consumeFreeAiQuota(env, subject) {
|
|
324
|
+
const now = new Date();
|
|
325
|
+
const day = now.toISOString().slice(0, 10);
|
|
326
|
+
const resetAt = Date.UTC(
|
|
327
|
+
now.getUTCFullYear(),
|
|
328
|
+
now.getUTCMonth(),
|
|
329
|
+
now.getUTCDate() + 1,
|
|
330
|
+
);
|
|
331
|
+
return stateRequest(env, "/quota/consume", {
|
|
332
|
+
// v4 resets counters from the pre-release response/prompt debugging rollout.
|
|
333
|
+
key: `v4:${subject}:${day}`,
|
|
334
|
+
limit: freeAiDailyCalls(env),
|
|
335
|
+
resetAt,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeMessages(value) {
|
|
340
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 8) {
|
|
341
|
+
throw Object.assign(new Error("1-8 chat messages are required"), { status: 400 });
|
|
342
|
+
}
|
|
343
|
+
let totalLength = 0;
|
|
344
|
+
const messages = value.map((message) => {
|
|
345
|
+
const role = String(message?.role || "");
|
|
346
|
+
const content = String(message?.content || "");
|
|
347
|
+
if (!["system", "user", "assistant"].includes(role) || !content) {
|
|
348
|
+
throw Object.assign(new Error("Each message needs a valid role and text content"), { status: 400 });
|
|
349
|
+
}
|
|
350
|
+
totalLength += content.length;
|
|
351
|
+
return { role, content };
|
|
352
|
+
});
|
|
353
|
+
if (totalLength > 50_000) {
|
|
354
|
+
throw Object.assign(new Error("AI prompt is too large"), { status: 413 });
|
|
355
|
+
}
|
|
356
|
+
return messages;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function roastSnapshot(request) {
|
|
360
|
+
const encoded = String(request.headers.get("x-vibe-roast-snapshot") || "");
|
|
361
|
+
if (!encoded) return null;
|
|
362
|
+
if (encoded.length > 8_000) {
|
|
363
|
+
throw Object.assign(new Error("Roast snapshot is too large"), { status: 413 });
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const snapshot = JSON.parse(new TextDecoder().decode(decodeBase64Url(encoded)));
|
|
367
|
+
if (
|
|
368
|
+
Number(snapshot?.snapshot_version) !== 1
|
|
369
|
+
|| !snapshot?.type_code
|
|
370
|
+
|| !Array.isArray(snapshot?.axes)
|
|
371
|
+
|| !Array.isArray(snapshot?.dimensions)
|
|
372
|
+
|| !Array.isArray(snapshot?.category_shares)
|
|
373
|
+
|| !Array.isArray(snapshot?.concepts)
|
|
374
|
+
) {
|
|
375
|
+
throw new Error("invalid shape");
|
|
376
|
+
}
|
|
377
|
+
return snapshot;
|
|
378
|
+
} catch {
|
|
379
|
+
throw Object.assign(new Error("Roast snapshot is invalid"), { status: 400 });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function rowsByKey(rows) {
|
|
384
|
+
return new Map((rows || []).map((row) => [String(row?.key || ""), row]));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function largestRowDelta(previousRows, nextRows, field) {
|
|
388
|
+
const previous = rowsByKey(previousRows);
|
|
389
|
+
const next = rowsByKey(nextRows);
|
|
390
|
+
const keys = new Set([...previous.keys(), ...next.keys()]);
|
|
391
|
+
let largest = 0;
|
|
392
|
+
for (const key of keys) {
|
|
393
|
+
largest = Math.max(
|
|
394
|
+
largest,
|
|
395
|
+
Math.abs(Number(previous.get(key)?.[field] || 0) - Number(next.get(key)?.[field] || 0)),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return largest;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function categoryDistance(previousRows, nextRows) {
|
|
402
|
+
const previous = rowsByKey(previousRows);
|
|
403
|
+
const next = rowsByKey(nextRows);
|
|
404
|
+
const keys = new Set([...previous.keys(), ...next.keys()]);
|
|
405
|
+
let total = 0;
|
|
406
|
+
for (const key of keys) {
|
|
407
|
+
total += Math.abs(Number(previous.get(key)?.share || 0) - Number(next.get(key)?.share || 0));
|
|
408
|
+
}
|
|
409
|
+
return total / 2;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function conceptOverlap(previousConcepts, nextConcepts) {
|
|
413
|
+
const previous = new Set((previousConcepts || []).map(String));
|
|
414
|
+
const next = new Set((nextConcepts || []).map(String));
|
|
415
|
+
const union = new Set([...previous, ...next]);
|
|
416
|
+
if (!union.size) return 1;
|
|
417
|
+
let intersection = 0;
|
|
418
|
+
for (const concept of previous) if (next.has(concept)) intersection += 1;
|
|
419
|
+
return intersection / union.size;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function isMaterialRoastChange(previous, next) {
|
|
423
|
+
if (!previous || !next) return true;
|
|
424
|
+
if (
|
|
425
|
+
previous.snapshot_version !== next.snapshot_version
|
|
426
|
+
|| previous.writer_prompt_version !== next.writer_prompt_version
|
|
427
|
+
|| previous.evidence_schema_version !== next.evidence_schema_version
|
|
428
|
+
|| previous.type_code !== next.type_code
|
|
429
|
+
|| previous.status !== next.status
|
|
430
|
+
) return true;
|
|
431
|
+
if (Math.abs(Number(previous.confidence || 0) - Number(next.confidence || 0)) >= 20) return true;
|
|
432
|
+
|
|
433
|
+
const previousAxes = rowsByKey(previous.axes);
|
|
434
|
+
const nextAxes = rowsByKey(next.axes);
|
|
435
|
+
if (previousAxes.size !== nextAxes.size) return true;
|
|
436
|
+
for (const [key, axis] of previousAxes) {
|
|
437
|
+
const nextAxis = nextAxes.get(key);
|
|
438
|
+
if (!nextAxis || axis.chosen !== nextAxis.chosen) return true;
|
|
439
|
+
}
|
|
440
|
+
if (largestRowDelta(previous.axes, next.axes, "left_percent") >= 15) return true;
|
|
441
|
+
if (largestRowDelta(previous.dimensions, next.dimensions, "value") >= 18) return true;
|
|
442
|
+
if (categoryDistance(previous.category_shares, next.category_shares) >= 20) return true;
|
|
443
|
+
if (
|
|
444
|
+
previous.concepts?.length >= 4
|
|
445
|
+
&& next.concepts?.length >= 4
|
|
446
|
+
&& conceptOverlap(previous.concepts, next.concepts) < 0.4
|
|
447
|
+
) return true;
|
|
448
|
+
if (previous.top_agent && next.top_agent && previous.top_agent !== next.top_agent) return true;
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function cachedRoast(env, subject) {
|
|
453
|
+
return stateRequest(env, "/roast/get", { key: `v1:${String(subject).toLowerCase()}` });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function storeRoast(env, subject, value) {
|
|
457
|
+
return stateRequest(env, "/roast/put", {
|
|
458
|
+
key: `v1:${String(subject).toLowerCase()}`,
|
|
459
|
+
value,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function completionResponse(content, {
|
|
464
|
+
cache,
|
|
465
|
+
createdAt = Date.now(),
|
|
466
|
+
usage,
|
|
467
|
+
quota,
|
|
468
|
+
} = {}) {
|
|
469
|
+
const headers = {
|
|
470
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
471
|
+
"Cache-Control": "no-store",
|
|
472
|
+
"X-Vibe-Roast-Cache": cache,
|
|
473
|
+
};
|
|
474
|
+
if (quota) {
|
|
475
|
+
headers["X-RateLimit-Limit"] = String(quota.limit);
|
|
476
|
+
headers["X-RateLimit-Remaining"] = String(quota.remaining);
|
|
477
|
+
}
|
|
478
|
+
return new Response(JSON.stringify({
|
|
479
|
+
id: `chatcmpl-${randomToken(12)}`,
|
|
480
|
+
object: "chat.completion",
|
|
481
|
+
created: Math.floor(createdAt / 1000),
|
|
482
|
+
model: FREE_AI_MODEL,
|
|
483
|
+
choices: [{
|
|
484
|
+
index: 0,
|
|
485
|
+
message: { role: "assistant", content },
|
|
486
|
+
finish_reason: "stop",
|
|
487
|
+
}],
|
|
488
|
+
usage,
|
|
489
|
+
}), { status: 200, headers });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function handleFreeAiCompletion(request, env) {
|
|
493
|
+
if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
|
|
494
|
+
if (!env.AUTH_BROKER_SESSION_SECRET || !env.AI) {
|
|
495
|
+
return json({ error: "Free AI is not configured" }, 503);
|
|
496
|
+
}
|
|
497
|
+
const session = await verifySignedSession(
|
|
498
|
+
env.AUTH_BROKER_SESSION_SECRET,
|
|
499
|
+
bearerToken(request),
|
|
500
|
+
);
|
|
501
|
+
const body = await readSmallJson(request, MAX_AI_BODY);
|
|
502
|
+
const messages = normalizeMessages(body.messages);
|
|
503
|
+
const snapshot = roastSnapshot(request);
|
|
504
|
+
if (snapshot) {
|
|
505
|
+
const cached = await cachedRoast(env, session.sub).catch(() => null);
|
|
506
|
+
if (cached?.content && !isMaterialRoastChange(cached.snapshot, snapshot)) {
|
|
507
|
+
return completionResponse(cached.content, {
|
|
508
|
+
cache: "HIT",
|
|
509
|
+
createdAt: Number(cached.createdAt) || Date.now(),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const lastUserIndex = messages.findLastIndex((message) => message.role === "user");
|
|
514
|
+
if (lastUserIndex >= 0) {
|
|
515
|
+
messages[lastUserIndex] = {
|
|
516
|
+
...messages[lastUserIndex],
|
|
517
|
+
content: `${messages[lastUserIndex].content}\n\n/no_think`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const quota = await consumeFreeAiQuota(env, session.sub);
|
|
521
|
+
if (!quota?.allowed) {
|
|
522
|
+
return new Response(JSON.stringify({
|
|
523
|
+
error: {
|
|
524
|
+
message: "Daily free roast limit reached. Use your own provider key or try again after 00:00 UTC.",
|
|
525
|
+
type: "rate_limit_error",
|
|
526
|
+
},
|
|
527
|
+
}), {
|
|
528
|
+
status: 429,
|
|
529
|
+
headers: {
|
|
530
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
531
|
+
"Cache-Control": "no-store",
|
|
532
|
+
"Retry-After": String(Math.max(1, Math.ceil((quota.resetAt - Date.now()) / 1000))),
|
|
533
|
+
"X-RateLimit-Limit": String(quota.limit),
|
|
534
|
+
"X-RateLimit-Remaining": "0",
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const temperature = Math.max(0, Math.min(1.2, Number(body.temperature) || 0.72));
|
|
540
|
+
const maxTokens = Math.max(128, Math.min(1800, Number(body.max_tokens) || 1200));
|
|
541
|
+
const result = await env.AI.run(FREE_AI_MODEL, {
|
|
542
|
+
messages,
|
|
543
|
+
temperature,
|
|
544
|
+
max_tokens: maxTokens,
|
|
545
|
+
chat_template_kwargs: { enable_thinking: false },
|
|
546
|
+
});
|
|
547
|
+
const content = workersAiContent(result);
|
|
548
|
+
if (!content) {
|
|
549
|
+
throw Object.assign(new Error("Workers AI returned no roast content"), { status: 502 });
|
|
550
|
+
}
|
|
551
|
+
const createdAt = Date.now();
|
|
552
|
+
if (snapshot) {
|
|
553
|
+
await storeRoast(env, session.sub, {
|
|
554
|
+
snapshot,
|
|
555
|
+
content,
|
|
556
|
+
createdAt,
|
|
557
|
+
model: FREE_AI_MODEL,
|
|
558
|
+
}).catch(() => {});
|
|
559
|
+
}
|
|
560
|
+
return completionResponse(content, {
|
|
561
|
+
cache: "MISS",
|
|
562
|
+
createdAt,
|
|
563
|
+
usage: result?.usage,
|
|
564
|
+
quota,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function workersAiContent(result) {
|
|
569
|
+
if (typeof result === "string") return result;
|
|
570
|
+
const candidates = [
|
|
571
|
+
result?.response,
|
|
572
|
+
result?.choices?.[0]?.message?.content,
|
|
573
|
+
result?.choices?.[0]?.text,
|
|
574
|
+
result?.choices?.[0]?.message?.reasoning,
|
|
575
|
+
result?.choices?.[0]?.message?.reasoning_content,
|
|
576
|
+
];
|
|
577
|
+
for (const candidate of candidates) {
|
|
578
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate;
|
|
579
|
+
if (candidate && typeof candidate === "object") return JSON.stringify(candidate);
|
|
580
|
+
}
|
|
581
|
+
return "";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function handleExchange(request, env) {
|
|
585
|
+
if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
|
|
586
|
+
const body = await readSmallJson(request);
|
|
587
|
+
const ticket = await stateRequest(env, "/ticket/take", { key: String(body.ticket || "") });
|
|
588
|
+
if (
|
|
589
|
+
!ticket
|
|
590
|
+
|| ticket.expiresAt <= Date.now()
|
|
591
|
+
|| ticket.localState !== String(body.state || "")
|
|
592
|
+
|| ticket.callback !== String(body.callback || "")
|
|
593
|
+
) {
|
|
594
|
+
return json({ error: "The GitHub sign-in ticket is invalid or expired" }, 400);
|
|
595
|
+
}
|
|
596
|
+
return json({
|
|
597
|
+
sessionToken: ticket.sessionToken,
|
|
598
|
+
expiresAt: new Date(ticket.sessionExpiresAt).toISOString(),
|
|
599
|
+
user: ticket.user,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export class OAuthState {
|
|
604
|
+
constructor(ctx) {
|
|
605
|
+
this.storage = ctx.storage;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async take(key) {
|
|
609
|
+
return this.storage.transaction(async (transaction) => {
|
|
610
|
+
const value = await transaction.get(key);
|
|
611
|
+
if (value !== undefined) await transaction.delete(key);
|
|
612
|
+
return value ?? null;
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async fetch(request) {
|
|
617
|
+
if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
|
|
618
|
+
const url = new URL(request.url);
|
|
619
|
+
const body = await request.json().catch(() => ({}));
|
|
620
|
+
const key = String(body.key || "");
|
|
621
|
+
if (!key) return json({ error: "Storage key is required" }, 400);
|
|
622
|
+
const namespace = url.pathname.startsWith("/flow/")
|
|
623
|
+
? "flow"
|
|
624
|
+
: url.pathname.startsWith("/quota/")
|
|
625
|
+
? "quota"
|
|
626
|
+
: url.pathname.startsWith("/roast/")
|
|
627
|
+
? "roast"
|
|
628
|
+
: "ticket";
|
|
629
|
+
const storageKey = `${namespace}:${key}`;
|
|
630
|
+
|
|
631
|
+
if (url.pathname === "/quota/consume") {
|
|
632
|
+
const limit = Math.max(1, Number(body.limit) || DEFAULT_FREE_AI_DAILY_CALLS);
|
|
633
|
+
const resetAt = Number(body.resetAt) || Date.now() + 24 * 60 * 60 * 1000;
|
|
634
|
+
const value = await this.storage.transaction(async (transaction) => {
|
|
635
|
+
const current = await transaction.get(storageKey);
|
|
636
|
+
const count = Number(current?.count || 0);
|
|
637
|
+
if (count >= limit) {
|
|
638
|
+
return { allowed: false, limit, remaining: 0, resetAt };
|
|
639
|
+
}
|
|
640
|
+
await transaction.put(storageKey, { count: count + 1, resetAt });
|
|
641
|
+
return {
|
|
642
|
+
allowed: true,
|
|
643
|
+
limit,
|
|
644
|
+
remaining: Math.max(0, limit - count - 1),
|
|
645
|
+
resetAt,
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
return json({ value });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (url.pathname.endsWith("/put")) {
|
|
652
|
+
await this.storage.put(storageKey, body.value);
|
|
653
|
+
return json({ ok: true });
|
|
654
|
+
}
|
|
655
|
+
if (url.pathname.endsWith("/get")) {
|
|
656
|
+
return json({ value: await this.storage.get(storageKey) ?? null });
|
|
657
|
+
}
|
|
658
|
+
if (url.pathname.endsWith("/take")) {
|
|
659
|
+
return json({ value: await this.take(storageKey) });
|
|
660
|
+
}
|
|
661
|
+
return json({ error: "Not found" }, 404);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export default {
|
|
666
|
+
async fetch(request, env) {
|
|
667
|
+
try {
|
|
668
|
+
const url = new URL(request.url);
|
|
669
|
+
if (url.pathname === "/health") {
|
|
670
|
+
return json({
|
|
671
|
+
ok: true,
|
|
672
|
+
service: "vibe-roast-auth",
|
|
673
|
+
freeAi: Boolean(env.AI),
|
|
674
|
+
model: FREE_AI_MODEL,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (url.pathname === "/oauth/github/start" && request.method === "GET") {
|
|
678
|
+
return await handleStart(request, env);
|
|
679
|
+
}
|
|
680
|
+
if (url.pathname === "/oauth/github/callback" && request.method === "GET") {
|
|
681
|
+
return await handleCallback(request, env);
|
|
682
|
+
}
|
|
683
|
+
if (url.pathname === "/oauth/github/exchange") {
|
|
684
|
+
return await handleExchange(request, env);
|
|
685
|
+
}
|
|
686
|
+
if (url.pathname === "/v1/chat/completions") {
|
|
687
|
+
return await handleFreeAiCompletion(request, env);
|
|
688
|
+
}
|
|
689
|
+
return json({ error: "Not found" }, 404);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
return json({ error: error?.message || String(error) }, Number(error?.status) || 500);
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
};
|