@ottocode/server 0.1.173
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/package.json +42 -0
- package/src/events/bus.ts +43 -0
- package/src/events/types.ts +32 -0
- package/src/index.ts +281 -0
- package/src/openapi/helpers.ts +64 -0
- package/src/openapi/paths/ask.ts +70 -0
- package/src/openapi/paths/config.ts +218 -0
- package/src/openapi/paths/files.ts +72 -0
- package/src/openapi/paths/git.ts +457 -0
- package/src/openapi/paths/messages.ts +92 -0
- package/src/openapi/paths/sessions.ts +90 -0
- package/src/openapi/paths/setu.ts +154 -0
- package/src/openapi/paths/stream.ts +26 -0
- package/src/openapi/paths/terminals.ts +226 -0
- package/src/openapi/schemas.ts +345 -0
- package/src/openapi/spec.ts +49 -0
- package/src/presets.ts +85 -0
- package/src/routes/ask.ts +113 -0
- package/src/routes/auth.ts +592 -0
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +44 -0
- package/src/routes/config/cwd.ts +21 -0
- package/src/routes/config/defaults.ts +45 -0
- package/src/routes/config/index.ts +16 -0
- package/src/routes/config/main.ts +73 -0
- package/src/routes/config/models.ts +139 -0
- package/src/routes/config/providers.ts +46 -0
- package/src/routes/config/utils.ts +120 -0
- package/src/routes/files.ts +218 -0
- package/src/routes/git/branch.ts +75 -0
- package/src/routes/git/commit.ts +209 -0
- package/src/routes/git/diff.ts +137 -0
- package/src/routes/git/index.ts +18 -0
- package/src/routes/git/push.ts +160 -0
- package/src/routes/git/schemas.ts +48 -0
- package/src/routes/git/staging.ts +208 -0
- package/src/routes/git/status.ts +83 -0
- package/src/routes/git/types.ts +31 -0
- package/src/routes/git/utils.ts +249 -0
- package/src/routes/openapi.ts +6 -0
- package/src/routes/research.ts +392 -0
- package/src/routes/root.ts +5 -0
- package/src/routes/session-approval.ts +63 -0
- package/src/routes/session-files.ts +387 -0
- package/src/routes/session-messages.ts +170 -0
- package/src/routes/session-stream.ts +61 -0
- package/src/routes/sessions.ts +814 -0
- package/src/routes/setu.ts +346 -0
- package/src/routes/terminals.ts +227 -0
- package/src/runtime/agent/registry.ts +351 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +257 -0
- package/src/runtime/agent/runner.ts +375 -0
- package/src/runtime/agent-registry.ts +6 -0
- package/src/runtime/ask/service.ts +369 -0
- package/src/runtime/context/environment.ts +202 -0
- package/src/runtime/debug/index.ts +117 -0
- package/src/runtime/debug/state.ts +140 -0
- package/src/runtime/errors/api-error.ts +192 -0
- package/src/runtime/errors/handling.ts +199 -0
- package/src/runtime/message/compaction-auto.ts +154 -0
- package/src/runtime/message/compaction-context.ts +101 -0
- package/src/runtime/message/compaction-detect.ts +26 -0
- package/src/runtime/message/compaction-limits.ts +37 -0
- package/src/runtime/message/compaction-mark.ts +111 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +21 -0
- package/src/runtime/message/history-builder.ts +266 -0
- package/src/runtime/message/service.ts +468 -0
- package/src/runtime/message/tool-history-tracker.ts +204 -0
- package/src/runtime/prompt/builder.ts +167 -0
- package/src/runtime/provider/anthropic.ts +50 -0
- package/src/runtime/provider/copilot.ts +12 -0
- package/src/runtime/provider/google.ts +8 -0
- package/src/runtime/provider/index.ts +60 -0
- package/src/runtime/provider/moonshot.ts +8 -0
- package/src/runtime/provider/oauth-adapter.ts +237 -0
- package/src/runtime/provider/openai.ts +18 -0
- package/src/runtime/provider/opencode.ts +7 -0
- package/src/runtime/provider/openrouter.ts +7 -0
- package/src/runtime/provider/selection.ts +118 -0
- package/src/runtime/provider/setu.ts +126 -0
- package/src/runtime/provider/zai.ts +16 -0
- package/src/runtime/session/branch.ts +280 -0
- package/src/runtime/session/db-operations.ts +285 -0
- package/src/runtime/session/manager.ts +99 -0
- package/src/runtime/session/queue.ts +243 -0
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +371 -0
- package/src/runtime/stream/finish-handler.ts +101 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +25 -0
- package/src/runtime/tools/approval.ts +180 -0
- package/src/runtime/tools/context.ts +83 -0
- package/src/runtime/tools/mapping.ts +154 -0
- package/src/runtime/tools/setup.ts +44 -0
- package/src/runtime/topup/manager.ts +110 -0
- package/src/runtime/utils/cwd.ts +69 -0
- package/src/runtime/utils/token.ts +35 -0
- package/src/tools/adapter.ts +634 -0
- package/src/tools/database/get-parent-session.ts +183 -0
- package/src/tools/database/get-session-context.ts +161 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +126 -0
- package/src/tools/database/search-history.ts +135 -0
- package/src/types/sql-imports.d.ts +5 -0
- package/sst-env.d.ts +8 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import {
|
|
3
|
+
getAllAuth,
|
|
4
|
+
setAuth,
|
|
5
|
+
removeAuth,
|
|
6
|
+
ensureSetuWallet,
|
|
7
|
+
getSetuWallet,
|
|
8
|
+
importWallet,
|
|
9
|
+
loadConfig,
|
|
10
|
+
catalog,
|
|
11
|
+
getOnboardingComplete,
|
|
12
|
+
setOnboardingComplete,
|
|
13
|
+
authorize,
|
|
14
|
+
exchange,
|
|
15
|
+
authorizeWeb,
|
|
16
|
+
exchangeWeb,
|
|
17
|
+
authorizeOpenAI,
|
|
18
|
+
exchangeOpenAI,
|
|
19
|
+
authorizeCopilot,
|
|
20
|
+
pollForCopilotTokenOnce,
|
|
21
|
+
type ProviderId,
|
|
22
|
+
} from '@ottocode/sdk';
|
|
23
|
+
import { logger } from '@ottocode/sdk';
|
|
24
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
25
|
+
|
|
26
|
+
const oauthVerifiers = new Map<
|
|
27
|
+
string,
|
|
28
|
+
{ verifier: string; provider: string; createdAt: number; callbackUrl: string }
|
|
29
|
+
>();
|
|
30
|
+
|
|
31
|
+
const copilotDeviceSessions = new Map<
|
|
32
|
+
string,
|
|
33
|
+
{ deviceCode: string; interval: number; provider: string; createdAt: number }
|
|
34
|
+
>();
|
|
35
|
+
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
for (const [key, value] of oauthVerifiers.entries()) {
|
|
39
|
+
if (now - value.createdAt > 10 * 60 * 1000) {
|
|
40
|
+
oauthVerifiers.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const [key, value] of copilotDeviceSessions.entries()) {
|
|
44
|
+
if (now - value.createdAt > 10 * 60 * 1000) {
|
|
45
|
+
copilotDeviceSessions.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}, 60 * 1000);
|
|
49
|
+
|
|
50
|
+
export function registerAuthRoutes(app: Hono) {
|
|
51
|
+
app.get('/v1/auth/status', async (c) => {
|
|
52
|
+
try {
|
|
53
|
+
const projectRoot = process.cwd();
|
|
54
|
+
const auth = await getAllAuth(projectRoot);
|
|
55
|
+
const cfg = await loadConfig(projectRoot);
|
|
56
|
+
const onboardingComplete = await getOnboardingComplete(projectRoot);
|
|
57
|
+
const setuWallet = await getSetuWallet(projectRoot);
|
|
58
|
+
|
|
59
|
+
const providers: Record<
|
|
60
|
+
string,
|
|
61
|
+
{
|
|
62
|
+
configured: boolean;
|
|
63
|
+
type?: 'api' | 'oauth' | 'wallet';
|
|
64
|
+
label: string;
|
|
65
|
+
supportsOAuth: boolean;
|
|
66
|
+
modelCount: number;
|
|
67
|
+
costRange?: { min: number; max: number };
|
|
68
|
+
}
|
|
69
|
+
> = {};
|
|
70
|
+
|
|
71
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
72
|
+
const providerAuth = auth[id as ProviderId];
|
|
73
|
+
const models = entry.models || [];
|
|
74
|
+
const costs = models
|
|
75
|
+
.map((m) => m.cost?.input)
|
|
76
|
+
.filter((c): c is number => c !== undefined);
|
|
77
|
+
|
|
78
|
+
providers[id] = {
|
|
79
|
+
configured: !!providerAuth,
|
|
80
|
+
type: providerAuth?.type,
|
|
81
|
+
label: entry.label || id,
|
|
82
|
+
supportsOAuth:
|
|
83
|
+
id === 'anthropic' || id === 'openai' || id === 'copilot',
|
|
84
|
+
modelCount: models.length,
|
|
85
|
+
costRange:
|
|
86
|
+
costs.length > 0
|
|
87
|
+
? {
|
|
88
|
+
min: Math.min(...costs),
|
|
89
|
+
max: Math.max(...costs),
|
|
90
|
+
}
|
|
91
|
+
: undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return c.json({
|
|
96
|
+
onboardingComplete,
|
|
97
|
+
setu: setuWallet
|
|
98
|
+
? {
|
|
99
|
+
configured: true,
|
|
100
|
+
publicKey: setuWallet.publicKey,
|
|
101
|
+
}
|
|
102
|
+
: {
|
|
103
|
+
configured: false,
|
|
104
|
+
},
|
|
105
|
+
providers,
|
|
106
|
+
defaults: cfg.defaults,
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error('Failed to get auth status', error);
|
|
110
|
+
const errorResponse = serializeError(error);
|
|
111
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
app.post('/v1/auth/setu/setup', async (c) => {
|
|
116
|
+
try {
|
|
117
|
+
const projectRoot = process.cwd();
|
|
118
|
+
const existing = await getSetuWallet(projectRoot);
|
|
119
|
+
const wallet = await ensureSetuWallet(projectRoot);
|
|
120
|
+
|
|
121
|
+
return c.json({
|
|
122
|
+
success: true,
|
|
123
|
+
publicKey: wallet.publicKey,
|
|
124
|
+
isNew: !existing,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.error('Failed to setup Setu wallet', error);
|
|
128
|
+
const errorResponse = serializeError(error);
|
|
129
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
app.post('/v1/auth/setu/import', async (c) => {
|
|
134
|
+
try {
|
|
135
|
+
const { privateKey } = await c.req.json<{ privateKey: string }>();
|
|
136
|
+
|
|
137
|
+
if (!privateKey) {
|
|
138
|
+
return c.json({ error: 'Private key required' }, 400);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const wallet = importWallet(privateKey);
|
|
143
|
+
await setAuth(
|
|
144
|
+
'setu',
|
|
145
|
+
{ type: 'wallet', secret: privateKey },
|
|
146
|
+
undefined,
|
|
147
|
+
'global',
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return c.json({
|
|
151
|
+
success: true,
|
|
152
|
+
publicKey: wallet.publicKey,
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
return c.json({ error: 'Invalid private key format' }, 400);
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error('Failed to import Setu wallet', error);
|
|
159
|
+
const errorResponse = serializeError(error);
|
|
160
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
app.post('/v1/auth/:provider', async (c) => {
|
|
165
|
+
try {
|
|
166
|
+
const provider = c.req.param('provider') as ProviderId;
|
|
167
|
+
const { apiKey } = await c.req.json<{ apiKey: string }>();
|
|
168
|
+
|
|
169
|
+
if (!catalog[provider]) {
|
|
170
|
+
return c.json({ error: 'Unknown provider' }, 400);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!apiKey) {
|
|
174
|
+
return c.json({ error: 'API key required' }, 400);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await setAuth(
|
|
178
|
+
provider,
|
|
179
|
+
{ type: 'api', key: apiKey },
|
|
180
|
+
undefined,
|
|
181
|
+
'global',
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return c.json({ success: true, provider });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.error('Failed to add provider', error);
|
|
187
|
+
const errorResponse = serializeError(error);
|
|
188
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
app.post('/v1/auth/:provider/oauth/url', async (c) => {
|
|
193
|
+
try {
|
|
194
|
+
const provider = c.req.param('provider');
|
|
195
|
+
const { mode = 'max' } = await c.req
|
|
196
|
+
.json<{ mode?: string }>()
|
|
197
|
+
.catch(() => ({}));
|
|
198
|
+
|
|
199
|
+
let url: string;
|
|
200
|
+
let verifier: string;
|
|
201
|
+
|
|
202
|
+
if (provider === 'anthropic') {
|
|
203
|
+
const result = await authorize(mode as 'max' | 'console');
|
|
204
|
+
url = result.url;
|
|
205
|
+
verifier = result.verifier;
|
|
206
|
+
} else if (provider === 'openai') {
|
|
207
|
+
return c.json(
|
|
208
|
+
{
|
|
209
|
+
error:
|
|
210
|
+
'OpenAI OAuth requires localhost callback. Use the redirect flow instead.',
|
|
211
|
+
},
|
|
212
|
+
400,
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
return c.json(
|
|
216
|
+
{
|
|
217
|
+
error: `OAuth not supported for provider: ${provider}. Copilot uses device flow — use /v1/auth/copilot/device/start instead.`,
|
|
218
|
+
},
|
|
219
|
+
400,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const sessionId = crypto.randomUUID();
|
|
224
|
+
oauthVerifiers.set(sessionId, {
|
|
225
|
+
verifier,
|
|
226
|
+
provider,
|
|
227
|
+
createdAt: Date.now(),
|
|
228
|
+
callbackUrl: '',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return c.json({ url, sessionId, provider });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message =
|
|
234
|
+
error instanceof Error ? error.message : 'OAuth initialization failed';
|
|
235
|
+
logger.error('OAuth URL generation failed', error);
|
|
236
|
+
return c.json({ error: message }, 500);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
app.post('/v1/auth/:provider/oauth/exchange', async (c) => {
|
|
241
|
+
try {
|
|
242
|
+
const provider = c.req.param('provider');
|
|
243
|
+
const { code, sessionId } = await c.req.json<{
|
|
244
|
+
code: string;
|
|
245
|
+
sessionId: string;
|
|
246
|
+
}>();
|
|
247
|
+
|
|
248
|
+
if (!code || !sessionId) {
|
|
249
|
+
return c.json({ error: 'Code and sessionId required' }, 400);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!oauthVerifiers.has(sessionId)) {
|
|
253
|
+
return c.json({ error: 'Session expired or invalid' }, 400);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const verifierEntry = oauthVerifiers.get(sessionId);
|
|
257
|
+
if (!verifierEntry) {
|
|
258
|
+
return c.json({ error: 'Session expired or invalid' }, 400);
|
|
259
|
+
}
|
|
260
|
+
const { verifier } = verifierEntry;
|
|
261
|
+
oauthVerifiers.delete(sessionId);
|
|
262
|
+
|
|
263
|
+
if (provider === 'anthropic') {
|
|
264
|
+
const tokens = await exchange(code, verifier);
|
|
265
|
+
await setAuth(
|
|
266
|
+
'anthropic',
|
|
267
|
+
{
|
|
268
|
+
type: 'oauth',
|
|
269
|
+
refresh: tokens.refresh,
|
|
270
|
+
access: tokens.access,
|
|
271
|
+
expires: tokens.expires,
|
|
272
|
+
},
|
|
273
|
+
undefined,
|
|
274
|
+
'global',
|
|
275
|
+
);
|
|
276
|
+
} else if (provider === 'openai') {
|
|
277
|
+
return c.json({ error: 'Use redirect flow for OpenAI' }, 400);
|
|
278
|
+
} else {
|
|
279
|
+
return c.json({ error: 'Unknown provider' }, 400);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return c.json({ success: true, provider });
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const message =
|
|
285
|
+
error instanceof Error ? error.message : 'Token exchange failed';
|
|
286
|
+
logger.error('OAuth exchange failed', error);
|
|
287
|
+
return c.json({ error: message }, 500);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.get('/v1/auth/:provider/oauth/start', async (c) => {
|
|
292
|
+
try {
|
|
293
|
+
const provider = c.req.param('provider');
|
|
294
|
+
const mode = c.req.query('mode') || 'max';
|
|
295
|
+
|
|
296
|
+
let url: string;
|
|
297
|
+
let verifier: string;
|
|
298
|
+
let callbackUrl = '';
|
|
299
|
+
|
|
300
|
+
if (provider === 'anthropic') {
|
|
301
|
+
const host = c.req.header('host') || 'localhost:3000';
|
|
302
|
+
const protocol = c.req.header('x-forwarded-proto') || 'http';
|
|
303
|
+
callbackUrl = `${protocol}://${host}/v1/auth/${provider}/oauth/callback`;
|
|
304
|
+
const result = authorizeWeb(mode as 'max' | 'console', callbackUrl);
|
|
305
|
+
url = result.url;
|
|
306
|
+
verifier = result.verifier;
|
|
307
|
+
} else if (provider === 'openai') {
|
|
308
|
+
const result = await authorizeOpenAI();
|
|
309
|
+
url = result.url;
|
|
310
|
+
verifier = result.verifier;
|
|
311
|
+
callbackUrl = 'localhost';
|
|
312
|
+
result
|
|
313
|
+
.waitForCallback()
|
|
314
|
+
.then(async (code) => {
|
|
315
|
+
const tokens = await exchangeOpenAI(code, verifier);
|
|
316
|
+
await setAuth(
|
|
317
|
+
'openai',
|
|
318
|
+
{
|
|
319
|
+
type: 'oauth',
|
|
320
|
+
refresh: tokens.refresh,
|
|
321
|
+
access: tokens.access,
|
|
322
|
+
expires: tokens.expires,
|
|
323
|
+
accountId: tokens.accountId,
|
|
324
|
+
idToken: tokens.idToken,
|
|
325
|
+
},
|
|
326
|
+
undefined,
|
|
327
|
+
'global',
|
|
328
|
+
);
|
|
329
|
+
result.close();
|
|
330
|
+
})
|
|
331
|
+
.catch(() => {
|
|
332
|
+
result.close();
|
|
333
|
+
});
|
|
334
|
+
} else {
|
|
335
|
+
return c.json({ error: 'OAuth not supported for this provider' }, 400);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const sessionId = crypto.randomUUID();
|
|
339
|
+
oauthVerifiers.set(sessionId, {
|
|
340
|
+
verifier,
|
|
341
|
+
provider,
|
|
342
|
+
createdAt: Date.now(),
|
|
343
|
+
callbackUrl,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
c.header(
|
|
347
|
+
'Set-Cookie',
|
|
348
|
+
`oauth_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return c.redirect(url);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const message =
|
|
354
|
+
error instanceof Error ? error.message : 'OAuth initialization failed';
|
|
355
|
+
logger.error('OAuth start failed', error);
|
|
356
|
+
return c.json({ error: message }, 500);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
app.get('/v1/auth/:provider/oauth/callback', async (c) => {
|
|
361
|
+
try {
|
|
362
|
+
const provider = c.req.param('provider');
|
|
363
|
+
const code = c.req.query('code');
|
|
364
|
+
const fragment = c.req.query('fragment');
|
|
365
|
+
|
|
366
|
+
const cookies = c.req.header('Cookie') || '';
|
|
367
|
+
const sessionMatch = cookies.match(/oauth_session=([^;]+)/);
|
|
368
|
+
const sessionId = sessionMatch?.[1];
|
|
369
|
+
|
|
370
|
+
if (!sessionId || !oauthVerifiers.has(sessionId)) {
|
|
371
|
+
return c.html(
|
|
372
|
+
'<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const callbackEntry = oauthVerifiers.get(sessionId);
|
|
377
|
+
if (!callbackEntry) {
|
|
378
|
+
return c.html(
|
|
379
|
+
'<html><body><h1>Session expired</h1><p>Please close this window and try again.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const { verifier, callbackUrl } = callbackEntry;
|
|
383
|
+
oauthVerifiers.delete(sessionId);
|
|
384
|
+
|
|
385
|
+
if (provider === 'anthropic') {
|
|
386
|
+
const fullCode = fragment ? `${code}#${fragment}` : (code ?? '');
|
|
387
|
+
const tokens = await exchangeWeb(fullCode, verifier, callbackUrl);
|
|
388
|
+
|
|
389
|
+
await setAuth(
|
|
390
|
+
'anthropic',
|
|
391
|
+
{
|
|
392
|
+
type: 'oauth',
|
|
393
|
+
refresh: tokens.refresh,
|
|
394
|
+
access: tokens.access,
|
|
395
|
+
expires: tokens.expires,
|
|
396
|
+
},
|
|
397
|
+
undefined,
|
|
398
|
+
'global',
|
|
399
|
+
);
|
|
400
|
+
} else if (provider === 'openai') {
|
|
401
|
+
return c.html(
|
|
402
|
+
'<html><body><h1>OpenAI uses localhost callback</h1><p>This route is not used for OpenAI. Please close this window.</p><script>setTimeout(() => window.close(), 3000);</script></body></html>',
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return c.html(`
|
|
407
|
+
<html>
|
|
408
|
+
<head>
|
|
409
|
+
<title>Connected!</title>
|
|
410
|
+
<style>
|
|
411
|
+
body {
|
|
412
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
413
|
+
display: flex;
|
|
414
|
+
justify-content: center;
|
|
415
|
+
align-items: center;
|
|
416
|
+
height: 100vh;
|
|
417
|
+
margin: 0;
|
|
418
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
419
|
+
color: white;
|
|
420
|
+
}
|
|
421
|
+
.container {
|
|
422
|
+
text-align: center;
|
|
423
|
+
padding: 2rem;
|
|
424
|
+
background: rgba(255,255,255,0.1);
|
|
425
|
+
border-radius: 16px;
|
|
426
|
+
backdrop-filter: blur(10px);
|
|
427
|
+
}
|
|
428
|
+
.checkmark {
|
|
429
|
+
font-size: 4rem;
|
|
430
|
+
margin-bottom: 1rem;
|
|
431
|
+
}
|
|
432
|
+
h1 { margin: 0 0 0.5rem 0; }
|
|
433
|
+
p { margin: 0; opacity: 0.9; }
|
|
434
|
+
</style>
|
|
435
|
+
</head>
|
|
436
|
+
<body>
|
|
437
|
+
<div class="container">
|
|
438
|
+
<div class="checkmark">✓</div>
|
|
439
|
+
<h1>Connected!</h1>
|
|
440
|
+
<p>You can close this window.</p>
|
|
441
|
+
</div>
|
|
442
|
+
<script>
|
|
443
|
+
if (window.opener) {
|
|
444
|
+
window.opener.postMessage({ type: 'oauth-success', provider: '${provider}' }, '*');
|
|
445
|
+
}
|
|
446
|
+
setTimeout(() => window.close(), 1500);
|
|
447
|
+
</script>
|
|
448
|
+
</body>
|
|
449
|
+
</html>
|
|
450
|
+
`);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const message =
|
|
453
|
+
error instanceof Error ? error.message : 'Authentication failed';
|
|
454
|
+
logger.error('OAuth callback failed', error);
|
|
455
|
+
return c.html(`
|
|
456
|
+
<html>
|
|
457
|
+
<head>
|
|
458
|
+
<title>Error</title>
|
|
459
|
+
<style>
|
|
460
|
+
body {
|
|
461
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
462
|
+
display: flex;
|
|
463
|
+
justify-content: center;
|
|
464
|
+
align-items: center;
|
|
465
|
+
height: 100vh;
|
|
466
|
+
margin: 0;
|
|
467
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
468
|
+
color: white;
|
|
469
|
+
}
|
|
470
|
+
.container {
|
|
471
|
+
text-align: center;
|
|
472
|
+
padding: 2rem;
|
|
473
|
+
background: rgba(255,255,255,0.1);
|
|
474
|
+
border-radius: 16px;
|
|
475
|
+
backdrop-filter: blur(10px);
|
|
476
|
+
}
|
|
477
|
+
.icon { font-size: 4rem; margin-bottom: 1rem; }
|
|
478
|
+
h1 { margin: 0 0 0.5rem 0; }
|
|
479
|
+
p { margin: 0; opacity: 0.9; }
|
|
480
|
+
</style>
|
|
481
|
+
</head>
|
|
482
|
+
<body>
|
|
483
|
+
<div class="container">
|
|
484
|
+
<div class="icon">✗</div>
|
|
485
|
+
<h1>Error</h1>
|
|
486
|
+
<p>${message}</p>
|
|
487
|
+
</div>
|
|
488
|
+
<script>
|
|
489
|
+
if (window.opener) {
|
|
490
|
+
window.opener.postMessage({ type: 'oauth-error', provider: '${c.req.param('provider')}', error: '${message}' }, '*');
|
|
491
|
+
}
|
|
492
|
+
setTimeout(() => window.close(), 3000);
|
|
493
|
+
</script>
|
|
494
|
+
</body>
|
|
495
|
+
</html>
|
|
496
|
+
`);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
app.post('/v1/auth/copilot/device/start', async (c) => {
|
|
501
|
+
try {
|
|
502
|
+
const deviceData = await authorizeCopilot();
|
|
503
|
+
const sessionId = crypto.randomUUID();
|
|
504
|
+
copilotDeviceSessions.set(sessionId, {
|
|
505
|
+
deviceCode: deviceData.deviceCode,
|
|
506
|
+
interval: deviceData.interval,
|
|
507
|
+
provider: 'copilot',
|
|
508
|
+
createdAt: Date.now(),
|
|
509
|
+
});
|
|
510
|
+
return c.json({
|
|
511
|
+
sessionId,
|
|
512
|
+
userCode: deviceData.userCode,
|
|
513
|
+
verificationUri: deviceData.verificationUri,
|
|
514
|
+
interval: deviceData.interval,
|
|
515
|
+
});
|
|
516
|
+
} catch (error) {
|
|
517
|
+
const message =
|
|
518
|
+
error instanceof Error
|
|
519
|
+
? error.message
|
|
520
|
+
: 'Failed to start Copilot device flow';
|
|
521
|
+
logger.error('Copilot device flow start failed', error);
|
|
522
|
+
return c.json({ error: message }, 500);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
app.post('/v1/auth/copilot/device/poll', async (c) => {
|
|
527
|
+
try {
|
|
528
|
+
const { sessionId } = await c.req.json<{ sessionId: string }>();
|
|
529
|
+
if (!sessionId || !copilotDeviceSessions.has(sessionId)) {
|
|
530
|
+
return c.json({ error: 'Session expired or invalid' }, 400);
|
|
531
|
+
}
|
|
532
|
+
const session = copilotDeviceSessions.get(sessionId)!;
|
|
533
|
+
const result = await pollForCopilotTokenOnce(session.deviceCode);
|
|
534
|
+
if (result.status === 'complete') {
|
|
535
|
+
copilotDeviceSessions.delete(sessionId);
|
|
536
|
+
await setAuth(
|
|
537
|
+
'copilot',
|
|
538
|
+
{
|
|
539
|
+
type: 'oauth',
|
|
540
|
+
refresh: result.accessToken,
|
|
541
|
+
access: result.accessToken,
|
|
542
|
+
expires: 0,
|
|
543
|
+
},
|
|
544
|
+
undefined,
|
|
545
|
+
'global',
|
|
546
|
+
);
|
|
547
|
+
return c.json({ status: 'complete' });
|
|
548
|
+
}
|
|
549
|
+
if (result.status === 'pending') {
|
|
550
|
+
return c.json({ status: 'pending' });
|
|
551
|
+
}
|
|
552
|
+
if (result.status === 'error') {
|
|
553
|
+
copilotDeviceSessions.delete(sessionId);
|
|
554
|
+
return c.json({ status: 'error', error: result.error });
|
|
555
|
+
}
|
|
556
|
+
return c.json({ status: 'pending' });
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const message = error instanceof Error ? error.message : 'Poll failed';
|
|
559
|
+
logger.error('Copilot device poll failed', error);
|
|
560
|
+
return c.json({ error: message }, 500);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
app.post('/v1/auth/onboarding/complete', async (c) => {
|
|
565
|
+
try {
|
|
566
|
+
await setOnboardingComplete();
|
|
567
|
+
return c.json({ success: true });
|
|
568
|
+
} catch (error) {
|
|
569
|
+
logger.error('Failed to complete onboarding', error);
|
|
570
|
+
const errorResponse = serializeError(error);
|
|
571
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
app.delete('/v1/auth/:provider', async (c) => {
|
|
576
|
+
try {
|
|
577
|
+
const provider = c.req.param('provider') as ProviderId;
|
|
578
|
+
|
|
579
|
+
if (!catalog[provider]) {
|
|
580
|
+
return c.json({ error: 'Unknown provider' }, 400);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
await removeAuth(provider, undefined, 'global');
|
|
584
|
+
|
|
585
|
+
return c.json({ success: true, provider });
|
|
586
|
+
} catch (error) {
|
|
587
|
+
logger.error('Failed to remove provider', error);
|
|
588
|
+
const errorResponse = serializeError(error);
|
|
589
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { loadConfig } from '@ottocode/sdk';
|
|
3
|
+
import { getDb } from '@ottocode/database';
|
|
4
|
+
import { isProviderId, logger } from '@ottocode/sdk';
|
|
5
|
+
import {
|
|
6
|
+
createBranch,
|
|
7
|
+
listBranches,
|
|
8
|
+
getParentSession,
|
|
9
|
+
} from '../runtime/session/branch.ts';
|
|
10
|
+
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
11
|
+
|
|
12
|
+
export function registerBranchRoutes(app: Hono) {
|
|
13
|
+
app.post('/v1/sessions/:sessionId/branch', async (c) => {
|
|
14
|
+
try {
|
|
15
|
+
const sessionId = c.req.param('sessionId');
|
|
16
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
17
|
+
const cfg = await loadConfig(projectRoot);
|
|
18
|
+
const db = await getDb(cfg.projectRoot);
|
|
19
|
+
|
|
20
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
21
|
+
string,
|
|
22
|
+
unknown
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
const fromMessageId = body.fromMessageId;
|
|
26
|
+
if (typeof fromMessageId !== 'string' || !fromMessageId.trim()) {
|
|
27
|
+
return c.json({ error: 'fromMessageId is required' }, 400);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const provider =
|
|
31
|
+
typeof body.provider === 'string' && isProviderId(body.provider)
|
|
32
|
+
? body.provider
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
const model =
|
|
36
|
+
typeof body.model === 'string' && body.model.trim()
|
|
37
|
+
? body.model.trim()
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
const agent =
|
|
41
|
+
typeof body.agent === 'string' && body.agent.trim()
|
|
42
|
+
? body.agent.trim()
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
const title =
|
|
46
|
+
typeof body.title === 'string' && body.title.trim()
|
|
47
|
+
? body.title.trim()
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
const result = await createBranch({
|
|
51
|
+
db,
|
|
52
|
+
parentSessionId: sessionId,
|
|
53
|
+
fromMessageId: fromMessageId.trim(),
|
|
54
|
+
provider,
|
|
55
|
+
model,
|
|
56
|
+
agent,
|
|
57
|
+
title,
|
|
58
|
+
projectPath: cfg.projectRoot,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return c.json(result, 201);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger.error('Failed to create branch', err);
|
|
64
|
+
const errorResponse = serializeError(err);
|
|
65
|
+
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.get('/v1/sessions/:sessionId/branches', async (c) => {
|
|
70
|
+
try {
|
|
71
|
+
const sessionId = c.req.param('sessionId');
|
|
72
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
73
|
+
const cfg = await loadConfig(projectRoot);
|
|
74
|
+
const db = await getDb(cfg.projectRoot);
|
|
75
|
+
|
|
76
|
+
const branches = await listBranches(db, sessionId, cfg.projectRoot);
|
|
77
|
+
|
|
78
|
+
return c.json({ branches });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.error('Failed to list branches', err);
|
|
81
|
+
const errorResponse = serializeError(err);
|
|
82
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
app.get('/v1/sessions/:sessionId/parent', async (c) => {
|
|
87
|
+
try {
|
|
88
|
+
const sessionId = c.req.param('sessionId');
|
|
89
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
90
|
+
const cfg = await loadConfig(projectRoot);
|
|
91
|
+
const db = await getDb(cfg.projectRoot);
|
|
92
|
+
|
|
93
|
+
const parent = await getParentSession(db, sessionId, cfg.projectRoot);
|
|
94
|
+
|
|
95
|
+
if (!parent) {
|
|
96
|
+
return c.json({ parent: null });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return c.json({ parent });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logger.error('Failed to get parent session', err);
|
|
102
|
+
const errorResponse = serializeError(err);
|
|
103
|
+
return c.json(errorResponse, errorResponse.error.status || 500);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|