-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
173 lines (159 loc) · 6.03 KB
/
server.ts
File metadata and controls
173 lines (159 loc) · 6.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import "https://deno.land/std@0.208.0/dotenv/load.ts";
import { Server } from "@hocuspocus/server";
import { Database } from "@hocuspocus/extension-database";
import { DB } from "https://deno.land/x/sqlite@v3.9.1/mod.ts";
import { jwtVerify, createRemoteJWKSet } from "https://deno.land/x/jose@v4.14.4/index.ts";
// Path to your SQLite file (creates it if it doesn't exist)
const dbPath = Deno.env.get("DB_PATH") || "./hocuspocus.db";
// Initialize DB connection
const db = new DB(dbPath);
// Create table if it doesn't exist
db.execute(`
CREATE TABLE IF NOT EXISTS documents (
name TEXT PRIMARY KEY,
data BLOB
)
`);
// Google OAuth2 JWKS
const googleJWKS = createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
// Get Google Client ID from env or use default (with warning)
const GOOGLE_CLIENT_ID = Deno.env.get("GOOGLE_CLIENT_ID") || "671385367166-4118tll0ntluovkdm5agd85arvl1ml9h.apps.googleusercontent.com";
if (!Deno.env.get("GOOGLE_CLIENT_ID")) {
console.warn("⚠️ No GOOGLE_CLIENT_ID env var found. Using hardcoded default.");
}
// Get Gemini API key from env
const GEMINI_API_KEY = Deno.env.get("GEMINI_API_KEY");
if (!GEMINI_API_KEY) {
console.warn("⚠️ No GEMINI_API_KEY env var found. Gemini proxy endpoint will not work.");
}
// Verify Google JWT
async function verifyGoogleJWT(token: string) {
try {
const { payload } = await jwtVerify(token, googleJWKS, {
issuer: "https://accounts.google.com",
audience: GOOGLE_CLIENT_ID,
});
return payload;
} catch (error) {
console.error("JWT verification failed:", error);
throw new Error("Invalid token");
}
}
// Closure to expose db to the extension functions
const createServer = () => {
const server = new Server({
port: parseInt(Deno.env.get("PORT") || "1235"),
onRequest: async (request: Request): Promise<Response | undefined> => {
const url = new URL(request.url);
console.log('[onRequest] path=', url.pathname);
if (url.pathname === '/api/gemini') {
if (!GEMINI_API_KEY) {
console.error('[gemini] missing GEMINI_API_KEY');
return new Response(JSON.stringify({ error: 'GEMINI_API_KEY not configured' }), { status: 500 });
}
try {
const body = await request.json();
console.log('[gemini] forwarding payload keys=', Object.keys(body));
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await response.text();
console.log('[gemini] upstream status=', response.status);
return new Response(text, {
status: response.status,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json'
}
});
} catch (error) {
console.error('[gemini] error=', error && (error as Error).message);
return new Response(JSON.stringify({ error: (error as Error).message }), { status: 500 });
}
}
return undefined;
},
async onAuthenticate({ token }) {
if (!token) {
throw new Error("Authentication required");
}
const payload = await verifyGoogleJWT(token);
return {
user: {
id: payload.sub,
email: payload.email,
name: payload.name,
},
};
},
extensions: [
new Database({
// Fetch: Return Uint8Array or null
fetch: async ({ documentName }) => {
try {
const query = db.prepareQuery("SELECT data FROM documents WHERE name = ? ORDER BY rowid DESC");
const result = query.one([documentName]);
query.finalize();
return result ? result[0] : null;
} catch (error) {
return null;
}
},
// Store: Persist the updated Uint8Array (promisified)
store: async ({ documentName, state }) => { // state is Uint8Array
try {
const query = db.prepareQuery("INSERT INTO documents (name, data) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET data = ?");
query.execute([documentName, state, state]);
query.finalize();
} catch (error) {
console.error(`Store error for "${documentName}":`, error);
}
},
}),
],
});
// No additional middleware needed for static files
return server;
};
const server = createServer();
// Run Hocuspocus on 1235 to avoid conflict with the HTTP proxy
server.listen();
// Simple HTTP server for Gemini proxy on 8888
Deno.serve({ port: 8888 }, async (request: Request) => {
const url = new URL(request.url);
if (url.pathname === "/api/gemini" && request.method === "POST") {
if (!GEMINI_API_KEY) {
return new Response(JSON.stringify({ error: "GEMINI_API_KEY not configured" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
try {
const body = await request.json();
const upstream = await fetch(
`https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key=${GEMINI_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
const text = await upstream.text();
return new Response(text, {
status: upstream.status,
headers: {
"Content-Type": upstream.headers.get("Content-Type") || "application/json",
},
});
} catch (error) {
return new Response(JSON.stringify({ error: (error as Error).message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
return new Response("Welcome to Hocuspocus!", { status: 200 });
});
console.log("Hocuspocus server listening on http://localhost:1235");
console.log("Gemini proxy listening on http://localhost:8888/api/gemini");