auth-core-oauth.ts151 lines · main
1/**
2 * Full browser Google (and GitHub) OAuth start + callback for briven-engine.
3 * DOLTGRES user store; cookies set on redirect host when possible.
4 *
5 * GET /v1/auth-core/oauth/:provider/start?projectId=&redirect_uri=
6 * GET /v1/auth-core/oauth/:provider/callback?code=&state=
7 */
8
9import { Hono } from 'hono';
10
11import { BRIVEN_ENGINE_ID } from '../services/auth-core/engine.js';
12import {
13 getAuthorisationUrl,
14 signInUpWithCode,
15 type SupportedSocial,
16} from '../services/auth-core/thirdparty.js';
17import type { AppEnv } from '../types/app-env.js';
18
19export const authCoreOauthRouter = new Hono<AppEnv>();
20
21const PROVIDERS = new Set(['google', 'github']);
22
23authCoreOauthRouter.get('/v1/auth-core/oauth/:provider/start', async (c) => {
24 const provider = c.req.param('provider') as SupportedSocial;
25 if (!PROVIDERS.has(provider)) {
26 return c.json(
27 { engine: BRIVEN_ENGINE_ID, code: 'unsupported_provider' },
28 400,
29 );
30 }
31 const projectId = c.req.query('projectId') ?? undefined;
32 const redirectURI =
33 c.req.query('redirect_uri') ??
34 c.req.query('redirectURI') ??
35 `${new URL(c.req.url).origin}/v1/auth-core/oauth/${provider}/callback`;
36
37 // Encode projectId into state storage via getAuthorisationUrl (already stores projectId)
38 const result = await getAuthorisationUrl({
39 thirdPartyId: provider,
40 redirectURI,
41 projectId,
42 });
43 if (result.status !== 'OK') {
44 return c.json(result, 400);
45 }
46
47 // Optional: redirect browser straight to Google
48 if (c.req.query('redirect') === '1' || c.req.query('go') === '1') {
49 return c.redirect(result.urlWithQueryParams, 302);
50 }
51 return c.json({
52 engine: BRIVEN_ENGINE_ID,
53 storage: 'doltgres',
54 ...result,
55 callbackUrl: redirectURI,
56 });
57});
58
59authCoreOauthRouter.get('/v1/auth-core/oauth/:provider/callback', async (c) => {
60 const provider = c.req.param('provider') as SupportedSocial;
61 if (!PROVIDERS.has(provider)) {
62 return c.json(
63 { engine: BRIVEN_ENGINE_ID, code: 'unsupported_provider' },
64 400,
65 );
66 }
67 const code = c.req.query('code');
68 const state = c.req.query('state') ?? undefined;
69 const err = c.req.query('error');
70 if (err) {
71 return c.html(
72 `<!doctype html><html><body><p>OAuth error: ${escapeHtml(err)}</p></body></html>`,
73 400,
74 );
75 }
76 if (!code) {
77 return c.json(
78 { engine: BRIVEN_ENGINE_ID, code: 'code_required' },
79 400,
80 );
81 }
82
83 // redirect_uri must match what was used at start — reconstruct default
84 const redirectURI = `${new URL(c.req.url).origin}/v1/auth-core/oauth/${provider}/callback`;
85
86 // projectId is recovered from state map inside exchange
87 const result = await signInUpWithCode({
88 thirdPartyId: provider,
89 code,
90 redirectURI,
91 state,
92 });
93
94 if (result.status !== 'OK') {
95 return c.html(
96 `<!doctype html><html><body>
97 <p>Sign-in failed: ${escapeHtml(result.message)}</p>
98 <p>engine: briven-engine · storage: doltgres</p>
99 </body></html>`,
100 400,
101 );
102 }
103
104 // Set session cookies then show success (apps can replace with redirect)
105 c.header(
106 'Set-Cookie',
107 `sAccessToken=${result.session.accessToken}; Path=/; HttpOnly; SameSite=Lax`,
108 { append: true },
109 );
110 c.header(
111 'Set-Cookie',
112 `sRefreshToken=${result.session.refreshToken}; Path=/; HttpOnly; SameSite=Lax`,
113 { append: true },
114 );
115 c.header(
116 'Set-Cookie',
117 `sFrontToken=${encodeURIComponent(JSON.stringify({ uid: result.user.id, up: {} }))}; Path=/; SameSite=Lax`,
118 { append: true },
119 );
120
121 const appRedirect = c.req.query('app_redirect');
122 if (appRedirect && isSafeRedirect(appRedirect)) {
123 return c.redirect(appRedirect, 302);
124 }
125
126 return c.html(`<!doctype html><html><body style="font-family:monospace;padding:2rem">
127 <h1>Signed in with ${escapeHtml(provider)}</h1>
128 <p>user: ${escapeHtml(result.user.id)}</p>
129 <p>email: ${escapeHtml(result.user.email ?? '')}</p>
130 <p>tenant: ${escapeHtml(result.user.tenantId)}</p>
131 <p>engine: briven-engine · storage: doltgres</p>
132 <p>createdNewUser: ${result.createdNewUser}</p>
133 </body></html>`);
134});
135
136function escapeHtml(s: string): string {
137 return s
138 .replace(/&/g, '&amp;')
139 .replace(/</g, '&lt;')
140 .replace(/>/g, '&gt;')
141 .replace(/"/g, '&quot;');
142}
143
144function isSafeRedirect(url: string): boolean {
145 try {
146 const u = new URL(url);
147 return u.protocol === 'https:' || u.protocol === 'http:';
148 } catch {
149 return url.startsWith('/');
150 }
151}