flarecms 0.1.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.
Files changed (110) hide show
  1. package/README.md +73 -0
  2. package/dist/auth/index.js +40 -0
  3. package/dist/cli/commands.js +389 -0
  4. package/dist/cli/index.js +403 -0
  5. package/dist/cli/mcp.js +209 -0
  6. package/dist/db/index.js +164 -0
  7. package/dist/index.js +17626 -0
  8. package/package.json +105 -0
  9. package/scripts/fix-api-paths.mjs +32 -0
  10. package/scripts/fix-imports.mjs +38 -0
  11. package/scripts/prefix-css.mjs +45 -0
  12. package/src/api/lib/cache.ts +45 -0
  13. package/src/api/lib/response.ts +40 -0
  14. package/src/api/middlewares/auth.ts +186 -0
  15. package/src/api/middlewares/cors.ts +10 -0
  16. package/src/api/middlewares/rbac.ts +85 -0
  17. package/src/api/routes/auth.ts +377 -0
  18. package/src/api/routes/collections.ts +205 -0
  19. package/src/api/routes/content.ts +175 -0
  20. package/src/api/routes/device.ts +160 -0
  21. package/src/api/routes/magic.ts +150 -0
  22. package/src/api/routes/mcp.ts +273 -0
  23. package/src/api/routes/oauth.ts +160 -0
  24. package/src/api/routes/settings.ts +43 -0
  25. package/src/api/routes/setup.ts +307 -0
  26. package/src/api/routes/tokens.ts +80 -0
  27. package/src/api/schemas/auth.ts +15 -0
  28. package/src/api/schemas/index.ts +51 -0
  29. package/src/api/schemas/tokens.ts +24 -0
  30. package/src/auth/index.ts +28 -0
  31. package/src/cli/commands.ts +217 -0
  32. package/src/cli/index.ts +21 -0
  33. package/src/cli/mcp.ts +210 -0
  34. package/src/cli/tests/cli.test.ts +40 -0
  35. package/src/cli/tests/create.test.ts +87 -0
  36. package/src/client/FlareAdminRouter.tsx +47 -0
  37. package/src/client/app.tsx +175 -0
  38. package/src/client/components/app-sidebar.tsx +227 -0
  39. package/src/client/components/collection-modal.tsx +215 -0
  40. package/src/client/components/content-list.tsx +247 -0
  41. package/src/client/components/dynamic-form.tsx +190 -0
  42. package/src/client/components/field-modal.tsx +221 -0
  43. package/src/client/components/settings/api-token-section.tsx +400 -0
  44. package/src/client/components/settings/general-section.tsx +224 -0
  45. package/src/client/components/settings/security-section.tsx +154 -0
  46. package/src/client/components/settings/seo-section.tsx +200 -0
  47. package/src/client/components/settings/signup-section.tsx +257 -0
  48. package/src/client/components/ui/accordion.tsx +78 -0
  49. package/src/client/components/ui/avatar.tsx +107 -0
  50. package/src/client/components/ui/badge.tsx +52 -0
  51. package/src/client/components/ui/button.tsx +60 -0
  52. package/src/client/components/ui/card.tsx +103 -0
  53. package/src/client/components/ui/checkbox.tsx +27 -0
  54. package/src/client/components/ui/collapsible.tsx +19 -0
  55. package/src/client/components/ui/dialog.tsx +162 -0
  56. package/src/client/components/ui/icon-picker.tsx +485 -0
  57. package/src/client/components/ui/icons-data.ts +8476 -0
  58. package/src/client/components/ui/input.tsx +20 -0
  59. package/src/client/components/ui/label.tsx +20 -0
  60. package/src/client/components/ui/popover.tsx +91 -0
  61. package/src/client/components/ui/select.tsx +204 -0
  62. package/src/client/components/ui/separator.tsx +23 -0
  63. package/src/client/components/ui/sheet.tsx +141 -0
  64. package/src/client/components/ui/sidebar.tsx +722 -0
  65. package/src/client/components/ui/skeleton.tsx +13 -0
  66. package/src/client/components/ui/sonner.tsx +47 -0
  67. package/src/client/components/ui/switch.tsx +30 -0
  68. package/src/client/components/ui/table.tsx +116 -0
  69. package/src/client/components/ui/tabs.tsx +80 -0
  70. package/src/client/components/ui/textarea.tsx +18 -0
  71. package/src/client/components/ui/tooltip.tsx +68 -0
  72. package/src/client/hooks/use-mobile.ts +19 -0
  73. package/src/client/index.css +149 -0
  74. package/src/client/index.ts +7 -0
  75. package/src/client/layouts/admin-layout.tsx +93 -0
  76. package/src/client/layouts/settings-layout.tsx +104 -0
  77. package/src/client/lib/api.ts +72 -0
  78. package/src/client/lib/utils.ts +6 -0
  79. package/src/client/main.tsx +10 -0
  80. package/src/client/pages/collection-detail.tsx +634 -0
  81. package/src/client/pages/collections.tsx +180 -0
  82. package/src/client/pages/dashboard.tsx +133 -0
  83. package/src/client/pages/device.tsx +66 -0
  84. package/src/client/pages/document-detail-page.tsx +139 -0
  85. package/src/client/pages/documents-page.tsx +103 -0
  86. package/src/client/pages/login.tsx +345 -0
  87. package/src/client/pages/settings.tsx +65 -0
  88. package/src/client/pages/setup.tsx +129 -0
  89. package/src/client/pages/signup.tsx +188 -0
  90. package/src/client/store/auth.ts +30 -0
  91. package/src/client/store/collections.ts +13 -0
  92. package/src/client/store/config.ts +12 -0
  93. package/src/client/store/fetcher.ts +30 -0
  94. package/src/client/store/router.ts +95 -0
  95. package/src/client/store/schema.ts +39 -0
  96. package/src/client/store/settings.ts +31 -0
  97. package/src/client/types.ts +34 -0
  98. package/src/db/dynamic.ts +70 -0
  99. package/src/db/index.ts +16 -0
  100. package/src/db/migrations/001_initial_schema.ts +57 -0
  101. package/src/db/migrations/002_auth_tables.ts +84 -0
  102. package/src/db/migrator.ts +61 -0
  103. package/src/db/schema.ts +142 -0
  104. package/src/index.ts +12 -0
  105. package/src/server/index.ts +66 -0
  106. package/src/types.ts +20 -0
  107. package/style.css.d.ts +8 -0
  108. package/tests/css.test.ts +21 -0
  109. package/tests/modular.test.ts +29 -0
  110. package/tsconfig.json +10 -0
@@ -0,0 +1,273 @@
1
+ import { Hono } from 'hono';
2
+
3
+ import { setupMiddleware, authMiddleware } from '../middlewares/auth';
4
+ import { createDb, ensureUniqueSlug, createCollectionTable, addFieldToTable } from '../../db';
5
+ import { ulid } from 'ulidx';
6
+ import { dynamicContentSchema, collectionSchema, fieldSchema } from '../schemas';
7
+ import { sql } from 'kysely';
8
+ import { cache } from '../lib/cache';
9
+ import type { Bindings, Variables } from 'src/types';
10
+
11
+ export const mcpRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
12
+
13
+ mcpRoutes.use('*', setupMiddleware);
14
+ mcpRoutes.use('*', authMiddleware);
15
+
16
+ /**
17
+ * In Cloudflare Workers, long-polling / SSE streams combined with separate POST endpoints can fail
18
+ * because a POST might route to a different worker isolate than the GET SSE connection, dropping the message.
19
+ *
20
+ * FlareCMS implements a "Stateless RPC Endpoint" that allows specialized proxy agents to
21
+ * execute standard MCP-like tool calls synchronously. This works 100% within the Edge environment.
22
+ */
23
+
24
+ mcpRoutes.post("/execute", async (c) => {
25
+ try {
26
+ const db = createDb(c.env.DB);
27
+ const body = await c.req.json();
28
+ const { tool, arguments: args } = body;
29
+
30
+ if (!tool) {
31
+ return c.json({ error: "No tool specified" }, 400);
32
+ }
33
+
34
+ if (tool === "list_collections") {
35
+ const collections = await db.selectFrom('fc_collections').selectAll().execute();
36
+ return c.json({
37
+ content: [{ type: "text", text: JSON.stringify(collections, null, 2) }]
38
+ });
39
+ }
40
+
41
+ if (tool === "read_content") {
42
+ const collectionSlug = args?.collection as string;
43
+ const limit = (args?.limit as number) || 10;
44
+
45
+ if (!collectionSlug) return c.json({ error: "Missing 'collection' argument" }, 400);
46
+
47
+ const collectionRecord = await db.selectFrom('fc_collections')
48
+ .select('id')
49
+ .where('slug', '=', collectionSlug)
50
+ .executeTakeFirst();
51
+
52
+ if (!collectionRecord) {
53
+ return c.json({ content: [{ type: "text", text: `Error: Collection '${collectionSlug}' not found.` }] });
54
+ }
55
+
56
+ const tableName = `ec_${collectionSlug}`;
57
+
58
+ const content = await db.selectFrom(tableName as any)
59
+ .selectAll()
60
+ .where('status', '!=', 'deleted')
61
+ .limit(limit)
62
+ .execute().catch(() => []); // Graceful fail if table doesn't exist yet
63
+
64
+ return c.json({ content: [{ type: "text", text: JSON.stringify(content, null, 2) }] });
65
+ }
66
+
67
+ if (tool === "get_collection_schema") {
68
+ const collectionSlug = args?.collection as string;
69
+ if (!collectionSlug) return c.json({ error: "Missing 'collection' argument" }, 400);
70
+
71
+ const collection = await db.selectFrom('fc_collections')
72
+ .selectAll()
73
+ .where('slug', '=', collectionSlug)
74
+ .executeTakeFirst();
75
+
76
+ if (!collection) {
77
+ return c.json({ content: [{ type: "text", text: `Error: Collection '${collectionSlug}' not found.` }] });
78
+ }
79
+
80
+ const fields = await db.selectFrom('fc_fields')
81
+ .selectAll()
82
+ .where('collection_id', '=', collection.id)
83
+ .execute();
84
+
85
+ const schema = {
86
+ metadata: {
87
+ ...collection,
88
+ features: collection.features ? JSON.parse(collection.features) : []
89
+ },
90
+ fields
91
+ };
92
+
93
+ return c.json({
94
+ content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
95
+ });
96
+ }
97
+
98
+ if (tool === "create_document") {
99
+ const collectionName = args?.collection as string;
100
+ const data = args?.data;
101
+
102
+ if (!collectionName || !data) {
103
+ return c.json({ error: "Missing 'collection' or 'data' argument" }, 400);
104
+ }
105
+
106
+ const collection = await db.selectFrom('fc_collections')
107
+ .select('id')
108
+ .where('slug', '=', collectionName)
109
+ .executeTakeFirst();
110
+
111
+ if (!collection) return c.json({ error: `Collection '${collectionName}' not found` }, 404);
112
+
113
+ const parsed = dynamicContentSchema.safeParse(data);
114
+ if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
115
+
116
+ const id = ulid();
117
+ const docData = parsed.data;
118
+
119
+ const baseSlug = docData.slug || docData.title?.toLowerCase().replace(/[^a-z0-9]+/g, '-') || id;
120
+ const slug = await ensureUniqueSlug(db, collectionName, baseSlug);
121
+ const status = docData.status || 'draft';
122
+
123
+ const doc = {
124
+ ...docData,
125
+ id,
126
+ slug,
127
+ status,
128
+ };
129
+
130
+ await db.insertInto(`ec_${collectionName}` as any)
131
+ .values(doc)
132
+ .execute();
133
+
134
+ return c.json({
135
+ content: [{ type: "text", text: `Success: Document created with ID ${id} and slug ${slug}` }]
136
+ });
137
+ }
138
+
139
+ if (tool === "update_document") {
140
+ const collectionName = args?.collection as string;
141
+ const id = args?.id as string;
142
+ const data = args?.data;
143
+
144
+ if (!collectionName || !id || !data) {
145
+ return c.json({ error: "Missing 'collection', 'id', or 'data' argument" }, 400);
146
+ }
147
+
148
+ const parsed = dynamicContentSchema.safeParse(data);
149
+ if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
150
+
151
+ // Handle slug change uniqueness
152
+ let finalData = { ...parsed.data };
153
+ if (finalData.slug) {
154
+ finalData.slug = await ensureUniqueSlug(db, collectionName, finalData.slug, id);
155
+ }
156
+
157
+ await db.updateTable(`ec_${collectionName}` as any)
158
+ .set({
159
+ ...finalData,
160
+ updated_at: sql`CURRENT_TIMESTAMP`
161
+ })
162
+ .where('id', '=', id)
163
+ .execute();
164
+
165
+ return c.json({
166
+ content: [{ type: "text", text: `Success: Document ${id} updated.` }]
167
+ });
168
+ }
169
+
170
+ if (tool === "create_collection") {
171
+ const data = args;
172
+ const parsed = collectionSchema.safeParse(data);
173
+ if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
174
+
175
+ const id = ulid();
176
+ const slug = parsed.data.slug;
177
+
178
+ await db.insertInto('fc_collections')
179
+ .values({
180
+ id,
181
+ slug,
182
+ label: parsed.data.label,
183
+ label_singular: parsed.data.labelSingular || null,
184
+ description: parsed.data.description || null,
185
+ icon: parsed.data.icon || null,
186
+ is_public: parsed.data.isPublic ? 1 : 0,
187
+ })
188
+ .execute();
189
+
190
+ await createCollectionTable(db, slug);
191
+
192
+ // Sync cache
193
+ await cache.invalidateSchema(c.env.KV, slug);
194
+ await cache.invalidateCollectionList(c.env.KV);
195
+
196
+ return c.json({
197
+ content: [{ type: "text", text: `Success: Collection '${slug}' created with ID ${id}` }]
198
+ });
199
+ }
200
+
201
+ if (tool === "update_collection") {
202
+ const id = args?.id as string;
203
+ const data = args?.data;
204
+
205
+ if (!id || !data) return c.json({ error: "Missing 'id' or 'data' argument" }, 400);
206
+
207
+ await db.updateTable('fc_collections')
208
+ .set({
209
+ ...data,
210
+ updated_at: sql`CURRENT_TIMESTAMP`
211
+ })
212
+ .where('id', '=', id)
213
+ .execute();
214
+
215
+ // Sync cache
216
+ const updatedCol = await db.selectFrom('fc_collections')
217
+ .select('slug')
218
+ .where('id', '=', id)
219
+ .executeTakeFirst();
220
+
221
+ if (updatedCol) {
222
+ await cache.invalidateSchema(c.env.KV, updatedCol.slug);
223
+ await cache.invalidateCollectionList(c.env.KV);
224
+ }
225
+
226
+ return c.json({
227
+ content: [{ type: "text", text: `Success: Collection ${id} updated.` }]
228
+ });
229
+ }
230
+
231
+ if (tool === "add_field") {
232
+ const collectionId = args?.collection_id as string;
233
+ const data = args?.field;
234
+
235
+ if (!collectionId || !data) return c.json({ error: "Missing 'collection_id' or 'field' argument" }, 400);
236
+
237
+ const parsed = fieldSchema.safeParse(data);
238
+ if (!parsed.success) return c.json({ error: parsed.error.format() }, 400);
239
+
240
+ const collection = await db.selectFrom('fc_collections')
241
+ .select('slug')
242
+ .where('id', '=', collectionId)
243
+ .executeTakeFirst();
244
+
245
+ if (!collection) return c.json({ error: 'Collection not found' }, 404);
246
+
247
+ const fieldId = ulid();
248
+ await db.insertInto('fc_fields')
249
+ .values({
250
+ id: fieldId,
251
+ collection_id: collectionId,
252
+ slug: parsed.data.slug,
253
+ label: parsed.data.label,
254
+ type: parsed.data.type,
255
+ required: parsed.data.required ? 1 : 0,
256
+ })
257
+ .execute();
258
+
259
+ await addFieldToTable(db, collection.slug, parsed.data.slug, parsed.data.type);
260
+
261
+ // Sync cache
262
+ await cache.invalidateSchema(c.env.KV, collection.slug);
263
+
264
+ return c.json({
265
+ content: [{ type: "text", text: `Success: Field '${parsed.data.slug}' added to collection '${collection.slug}'` }]
266
+ });
267
+ }
268
+
269
+ return c.json({ error: "Tool not found" }, 404);
270
+ } catch (error: any) {
271
+ return c.json({ error: `Server Error: ${error.message}` }, 500);
272
+ }
273
+ });
@@ -0,0 +1,160 @@
1
+ import { Hono } from 'hono';
2
+ import { createDb } from '../../db';
3
+ import { generateSessionToken } from '../../auth';
4
+ import { setCookie } from 'hono/cookie';
5
+ import { oauthCallbackSchema } from '../schemas/auth';
6
+ import { ulid } from 'ulidx';
7
+ import type { Bindings, Variables } from '../index';
8
+ import { apiResponse } from '../lib/response';
9
+
10
+ export const oauthRoutes = new Hono<{ Bindings: Bindings; Variables: Variables }>();
11
+
12
+ // OAuth Login (Initiate Redirect)
13
+ oauthRoutes.get('/github/login', (c) => {
14
+ const clientId = c.env.GITHUB_CLIENT_ID;
15
+ if (!clientId) return apiResponse.error(c, 'GitHub OAuth not configured', 500);
16
+
17
+ const redirectUri = encodeURIComponent(`https://${new URL(c.req.url).hostname}/api/oauth/github/callback`);
18
+ const scope = encodeURIComponent('read:user user:email');
19
+
20
+ // Create secure random state parameter here to store in cookies usually
21
+ const state = Math.random().toString(36).substring(2);
22
+
23
+ return c.redirect(`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`);
24
+ });
25
+
26
+ // OAuth Callback
27
+ oauthRoutes.get('/github/callback', async (c) => {
28
+ const code = c.req.query('code');
29
+ if (!code) return apiResponse.error(c, 'Missing code');
30
+
31
+ const clientId = c.env.GITHUB_CLIENT_ID;
32
+ const clientSecret = c.env.GITHUB_CLIENT_SECRET;
33
+
34
+ if (!clientId || !clientSecret) return apiResponse.error(c, 'GitHub OAuth not configured', 500);
35
+
36
+ const db = createDb(c.env.DB);
37
+
38
+ try {
39
+ // 1. Exchange Code for Access Token
40
+ const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Accept': 'application/json',
44
+ 'Content-Type': 'application/json'
45
+ },
46
+ body: JSON.stringify({
47
+ client_id: clientId,
48
+ client_secret: clientSecret,
49
+ code
50
+ })
51
+ });
52
+ const tokenData: any = await tokenRes.json();
53
+ if (tokenData.error) throw new Error(tokenData.error_description || tokenData.error);
54
+
55
+ // 2. Fetch User Profile
56
+ const userRes = await fetch('https://api.github.com/user', {
57
+ headers: {
58
+ 'Authorization': `Bearer ${tokenData.access_token}`,
59
+ 'User-Agent': 'FlareCMS'
60
+ }
61
+ });
62
+ const userData: any = await userRes.json();
63
+
64
+ // 3. Fetch User Emails (GitHub separates primary email)
65
+ const emailRes = await fetch('https://api.github.com/user/emails', {
66
+ headers: {
67
+ 'Authorization': `Bearer ${tokenData.access_token}`,
68
+ 'User-Agent': 'FlareCMS'
69
+ }
70
+ });
71
+ const emailData: any[] = await emailRes.json();
72
+ const primaryEmail = emailData.find((e: any) => e.primary)?.email || userData.email;
73
+
74
+ if (!primaryEmail) throw new Error('No public email found in GitHub account');
75
+
76
+ // 4. Link or Provision User Account
77
+ const githubId = String(userData.id);
78
+ let user = await db.selectFrom('fc_oauth_accounts')
79
+ .innerJoin('fc_users', 'fc_oauth_accounts.user_id', 'fc_users.id')
80
+ .select(['fc_users.id', 'fc_users.disabled'])
81
+ .where('fc_oauth_accounts.provider_id', '=', 'github')
82
+ .where('fc_oauth_accounts.provider_user_id', '=', githubId)
83
+ .executeTakeFirst();
84
+
85
+ if (!user) {
86
+ // Look for existing user by email
87
+ let localUser = await db.selectFrom('fc_users').selectAll().where('email', '=', primaryEmail).executeTakeFirst();
88
+
89
+ if (!localUser) {
90
+ // 1. Check Registration Policy
91
+ const signupEnabled = await db.selectFrom('options').select('value').where('name', '=', 'flare:signup_enabled').executeTakeFirst();
92
+ const defaultRole = await db.selectFrom('options').select('value').where('name', '=', 'flare:signup_default_role').executeTakeFirst();
93
+ const domainRulesRaw = await db.selectFrom('options').select('value').where('name', '=', 'flare:signup_domain_rules').executeTakeFirst();
94
+
95
+ const isEnabled = signupEnabled?.value === 'true';
96
+ const roleDefault = defaultRole?.value || 'editor';
97
+ const domainRules = JSON.parse(domainRulesRaw?.value || '{}') as Record<string, string>;
98
+
99
+ if (!isEnabled) {
100
+ throw new Error('Signups are currently disabled');
101
+ }
102
+
103
+ // Determine role based on domain
104
+ const domain = primaryEmail.split('@')[1];
105
+ const assignedRole = domainRules[domain] || roleDefault;
106
+
107
+ // Provision new user
108
+ localUser = {
109
+ id: ulid(),
110
+ email: primaryEmail,
111
+ password: null,
112
+ role: assignedRole,
113
+ disabled: 0,
114
+ created_at: new Date().toISOString(),
115
+ updated_at: new Date().toISOString()
116
+ };
117
+ await db.insertInto('fc_users').values(localUser as any).execute();
118
+ }
119
+
120
+ // Link Account
121
+ await db.insertInto('fc_oauth_accounts')
122
+ .values({
123
+ provider_id: 'github',
124
+ provider_user_id: githubId,
125
+ user_id: localUser.id
126
+ })
127
+ .execute();
128
+
129
+ user = localUser as any;
130
+ }
131
+
132
+ if (user!.disabled) return apiResponse.error(c, 'Account is disabled', 403);
133
+
134
+ // 5. Create Session
135
+ const sessionId = generateSessionToken();
136
+ const expiresAt = new Date();
137
+ expiresAt.setDate(expiresAt.getDate() + 30);
138
+
139
+ await db.insertInto('fc_sessions')
140
+ .values({
141
+ id: sessionId,
142
+ user_id: user!.id,
143
+ expires_at: expiresAt.toISOString(),
144
+ })
145
+ .execute();
146
+
147
+ setCookie(c, 'session', sessionId, {
148
+ httpOnly: true,
149
+ secure: true,
150
+ sameSite: 'Strict',
151
+ expires: expiresAt,
152
+ path: '/'
153
+ });
154
+
155
+ return c.redirect('/admin'); // Assuming frontend handles redirect logic to dashboard
156
+ } catch (error: any) {
157
+ return apiResponse.error(c, error.message);
158
+ }
159
+ });
160
+
@@ -0,0 +1,43 @@
1
+ import { Hono } from 'hono';
2
+ import { createDb } from '../../db';
3
+ import { requireRole } from '../middlewares/rbac';
4
+ import type { Bindings } from '../index';
5
+ import { apiResponse } from '../lib/response';
6
+
7
+ export const settingsRoutes = new Hono<{ Bindings: Bindings }>();
8
+
9
+ // All settings operations require admin role
10
+ settingsRoutes.use('/*', requireRole(['admin']));
11
+
12
+ // Only admins can modify core settings
13
+ settingsRoutes.get('/', async (c) => {
14
+ const db = createDb(c.env.DB);
15
+ const result = await db.selectFrom('options')
16
+ .selectAll()
17
+ .where('name', 'like', 'flare:%')
18
+ .execute();
19
+ return apiResponse.ok(c, result);
20
+ });
21
+
22
+ settingsRoutes.patch('/', requireRole(['admin']), async (c) => {
23
+ const body = await c.req.json();
24
+ const db = createDb(c.env.DB);
25
+
26
+ try {
27
+ for (const [key, value] of Object.entries(body)) {
28
+ const settingName = key.startsWith('flare:') ? key : `flare:${key}`;
29
+ await db.insertInto('options')
30
+ .values({
31
+ name: settingName,
32
+ value: String(value)
33
+ })
34
+ .onConflict((oc) => oc.column('name').doUpdateSet({
35
+ value: String(value)
36
+ }))
37
+ .execute();
38
+ }
39
+ return apiResponse.ok(c, { success: true });
40
+ } catch (e: any) {
41
+ return apiResponse.error(c, e.message);
42
+ }
43
+ });