@gmickel/gno 0.6.1 → 0.7.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.
@@ -12,13 +12,19 @@ import { SqliteAdapter } from '../store/sqlite/adapter';
12
12
  import { createServerContext, disposeServerContext } from './context';
13
13
  // HTML import - Bun handles bundling TSX/CSS automatically via routes
14
14
  import homepage from './public/index.html';
15
+ import type { ContextHolder } from './routes/api';
15
16
  import {
16
17
  handleAsk,
17
18
  handleCapabilities,
18
19
  handleCollections,
20
+ handleCreateCollection,
21
+ handleCreateDoc,
22
+ handleDeactivateDoc,
23
+ handleDeleteCollection,
19
24
  handleDoc,
20
25
  handleDocs,
21
26
  handleHealth,
27
+ handleJob,
22
28
  handleModelPull,
23
29
  handleModelStatus,
24
30
  handlePresets,
@@ -26,7 +32,9 @@ import {
26
32
  handleSearch,
27
33
  handleSetPreset,
28
34
  handleStatus,
35
+ handleSync,
29
36
  } from './routes/api';
37
+ import { forbiddenResponse, isRequestAllowed } from './security';
30
38
 
31
39
  export interface ServeOptions {
32
40
  /** Port to listen on (default: 3000) */
@@ -127,9 +135,21 @@ export async function startServer(
127
135
  return { success: false, error: openResult.error.message };
128
136
  }
129
137
 
138
+ // Sync collections and contexts from config to DB (same as CLI initStore)
139
+ const syncCollResult = await store.syncCollections(config.collections);
140
+ if (!syncCollResult.ok) {
141
+ await store.close();
142
+ return { success: false, error: syncCollResult.error.message };
143
+ }
144
+ const syncCtxResult = await store.syncContexts(config.contexts ?? []);
145
+ if (!syncCtxResult.ok) {
146
+ await store.close();
147
+ return { success: false, error: syncCtxResult.error.message };
148
+ }
149
+
130
150
  // Create server context with LLM ports for hybrid search and AI answers
131
151
  // Use holder pattern to allow hot-reloading presets
132
- const ctxHolder = {
152
+ const ctxHolder: ContextHolder = {
133
153
  current: await createServerContext(store, config),
134
154
  config, // Keep original config for reloading
135
155
  };
@@ -158,7 +178,7 @@ export async function startServer(
158
178
  // Enable development mode for HMR and console logging
159
179
  development: isDev,
160
180
 
161
- // Routes object - Bun handles HTML bundling and /_bun/* assets automatically
181
+ // Static routes - Bun handles HTML bundling and /_bun/* assets automatically
162
182
  routes: {
163
183
  // SPA routes - all serve the same React app
164
184
  '/': homepage,
@@ -167,7 +187,7 @@ export async function startServer(
167
187
  '/doc': homepage,
168
188
  '/ask': homepage,
169
189
 
170
- // API routes
190
+ // API routes with CSRF protection wrapper
171
191
  '/api/health': {
172
192
  GET: () => withSecurityHeaders(handleHealth(), isDev),
173
193
  },
@@ -178,12 +198,56 @@ export async function startServer(
178
198
  '/api/collections': {
179
199
  GET: async () =>
180
200
  withSecurityHeaders(await handleCollections(store), isDev),
201
+ POST: async (req: Request) => {
202
+ if (!isRequestAllowed(req, port)) {
203
+ return withSecurityHeaders(forbiddenResponse(), isDev);
204
+ }
205
+ return withSecurityHeaders(
206
+ await handleCreateCollection(ctxHolder, store, req),
207
+ isDev
208
+ );
209
+ },
210
+ },
211
+ '/api/sync': {
212
+ POST: async (req: Request) => {
213
+ if (!isRequestAllowed(req, port)) {
214
+ return withSecurityHeaders(forbiddenResponse(), isDev);
215
+ }
216
+ return withSecurityHeaders(
217
+ await handleSync(ctxHolder, store, req),
218
+ isDev
219
+ );
220
+ },
181
221
  },
182
222
  '/api/docs': {
183
223
  GET: async (req: Request) => {
184
224
  const url = new URL(req.url);
185
225
  return withSecurityHeaders(await handleDocs(store, url), isDev);
186
226
  },
227
+ POST: async (req: Request) => {
228
+ if (!isRequestAllowed(req, port)) {
229
+ return withSecurityHeaders(forbiddenResponse(), isDev);
230
+ }
231
+ return withSecurityHeaders(
232
+ await handleCreateDoc(ctxHolder, store, req),
233
+ isDev
234
+ );
235
+ },
236
+ },
237
+ '/api/docs/:id/deactivate': {
238
+ POST: async (req: Request) => {
239
+ if (!isRequestAllowed(req, port)) {
240
+ return withSecurityHeaders(forbiddenResponse(), isDev);
241
+ }
242
+ const url = new URL(req.url);
243
+ // Extract id from /api/docs/:id/deactivate
244
+ const parts = url.pathname.split('/');
245
+ const id = decodeURIComponent(parts[3] || '');
246
+ return withSecurityHeaders(
247
+ await handleDeactivateDoc(store, id),
248
+ isDev
249
+ );
250
+ },
187
251
  },
188
252
  '/api/doc': {
189
253
  GET: async (req: Request) => {
@@ -192,19 +256,34 @@ export async function startServer(
192
256
  },
193
257
  },
194
258
  '/api/search': {
195
- POST: async (req: Request) =>
196
- withSecurityHeaders(await handleSearch(store, req), isDev),
259
+ POST: async (req: Request) => {
260
+ if (!isRequestAllowed(req, port)) {
261
+ return withSecurityHeaders(forbiddenResponse(), isDev);
262
+ }
263
+ return withSecurityHeaders(await handleSearch(store, req), isDev);
264
+ },
197
265
  },
198
266
  '/api/query': {
199
- POST: async (req: Request) =>
200
- withSecurityHeaders(
267
+ POST: async (req: Request) => {
268
+ if (!isRequestAllowed(req, port)) {
269
+ return withSecurityHeaders(forbiddenResponse(), isDev);
270
+ }
271
+ return withSecurityHeaders(
201
272
  await handleQuery(ctxHolder.current, req),
202
273
  isDev
203
- ),
274
+ );
275
+ },
204
276
  },
205
277
  '/api/ask': {
206
- POST: async (req: Request) =>
207
- withSecurityHeaders(await handleAsk(ctxHolder.current, req), isDev),
278
+ POST: async (req: Request) => {
279
+ if (!isRequestAllowed(req, port)) {
280
+ return withSecurityHeaders(forbiddenResponse(), isDev);
281
+ }
282
+ return withSecurityHeaders(
283
+ await handleAsk(ctxHolder.current, req),
284
+ isDev
285
+ );
286
+ },
208
287
  },
209
288
  '/api/capabilities': {
210
289
  GET: () =>
@@ -213,18 +292,50 @@ export async function startServer(
213
292
  '/api/presets': {
214
293
  GET: () =>
215
294
  withSecurityHeaders(handlePresets(ctxHolder.current), isDev),
216
- POST: async (req: Request) =>
217
- withSecurityHeaders(await handleSetPreset(ctxHolder, req), isDev),
295
+ POST: async (req: Request) => {
296
+ if (!isRequestAllowed(req, port)) {
297
+ return withSecurityHeaders(forbiddenResponse(), isDev);
298
+ }
299
+ return withSecurityHeaders(
300
+ await handleSetPreset(ctxHolder, req),
301
+ isDev
302
+ );
303
+ },
218
304
  },
219
305
  '/api/models/status': {
220
306
  GET: () => withSecurityHeaders(handleModelStatus(), isDev),
221
307
  },
222
308
  '/api/models/pull': {
223
- POST: () => withSecurityHeaders(handleModelPull(ctxHolder), isDev),
309
+ POST: (req: Request) => {
310
+ if (!isRequestAllowed(req, port)) {
311
+ return withSecurityHeaders(forbiddenResponse(), isDev);
312
+ }
313
+ return withSecurityHeaders(handleModelPull(ctxHolder), isDev);
314
+ },
315
+ },
316
+ '/api/jobs/:id': {
317
+ GET: (req: Request) => {
318
+ const url = new URL(req.url);
319
+ const id = decodeURIComponent(url.pathname.split('/').pop() || '');
320
+ return withSecurityHeaders(handleJob(id), isDev);
321
+ },
322
+ },
323
+ '/api/collections/:name': {
324
+ DELETE: async (req: Request) => {
325
+ if (!isRequestAllowed(req, port)) {
326
+ return withSecurityHeaders(forbiddenResponse(), isDev);
327
+ }
328
+ const url = new URL(req.url);
329
+ const name = decodeURIComponent(
330
+ url.pathname.split('/').pop() || ''
331
+ );
332
+ return withSecurityHeaders(
333
+ await handleDeleteCollection(ctxHolder, store, name),
334
+ isDev
335
+ );
336
+ },
224
337
  },
225
338
  },
226
-
227
- // No fetch fallback - let Bun handle /_bun/* assets and return 404 for others
228
339
  });
229
340
  } catch (e) {
230
341
  await store.close();