flarecms 0.1.2 → 0.2.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.
@@ -12683,10 +12683,28 @@ async function down2(db) {
12683
12683
  await db.schema.dropTable("fc_device_codes").ifExists().execute();
12684
12684
  }
12685
12685
 
12686
+ // src/db/migrations/003_plugins.ts
12687
+ var exports_003_plugins = {};
12688
+ __export(exports_003_plugins, {
12689
+ up: () => up3,
12690
+ down: () => down3
12691
+ });
12692
+ async function up3(db) {
12693
+ await db.schema.createTable("fc_plugins").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("plugin_id", "text", (col) => col.notNull().unique()).addColumn("version", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("inactive")).addColumn("capabilities", "text").addColumn("allowed_hosts", "text").addColumn("storage_config", "text").addColumn("manifest", "text").addColumn("backend_code", "text").addColumn("installed_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).addColumn("activated_at", "text").execute();
12694
+ await db.schema.createTable("_fc_plugin_storage").ifNotExists().addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("collection", "text", (col) => col.notNull()).addColumn("id", "text", (col) => col.notNull()).addColumn("data", "text", (col) => col.notNull()).addColumn("updated_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).addPrimaryKeyConstraint("pk_plugin_storage", ["plugin_id", "collection", "id"]).execute();
12695
+ await db.schema.createTable("_fc_cron_tasks").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("schedule", "text", (col) => col.notNull()).addColumn("last_run_at", "text").addColumn("next_run_at", "text").addColumn("enabled", "integer", (col) => col.defaultTo(1)).execute();
12696
+ }
12697
+ async function down3(db) {
12698
+ await db.schema.dropTable("fc_plugins").ifExists().execute();
12699
+ await db.schema.dropTable("_fc_plugin_storage").ifExists().execute();
12700
+ await db.schema.dropTable("_fc_cron_tasks").ifExists().execute();
12701
+ }
12702
+
12686
12703
  // src/db/migrator.ts
12687
12704
  var STATIC_MIGRATIONS = {
12688
12705
  "001_initial_schema": exports_001_initial_schema,
12689
- "002_auth_tables": exports_002_auth_tables
12706
+ "002_auth_tables": exports_002_auth_tables,
12707
+ "003_plugins": exports_003_plugins
12690
12708
  };
12691
12709
  async function runMigrations(db) {
12692
12710
  await sql`
@@ -12725,7 +12743,7 @@ var createDb = (d1) => {
12725
12743
  };
12726
12744
 
12727
12745
  // src/server/index.ts
12728
- import { Hono as Hono11 } from "hono";
12746
+ import { Hono as Hono12 } from "hono";
12729
12747
 
12730
12748
  // src/api/middlewares/cors.ts
12731
12749
  import { cors } from "hono/cors";
@@ -12752,6 +12770,8 @@ var authMiddleware = async (c, next) => {
12752
12770
  c.set("scopes", ["*"]);
12753
12771
  return next();
12754
12772
  }
12773
+ if (!c.env?.DB)
12774
+ return await next();
12755
12775
  const db = createDb(c.env.DB);
12756
12776
  if (authHeader?.startsWith("Bearer ec_pat_")) {
12757
12777
  const rawToken = authHeader.split(" ")[1];
@@ -12817,6 +12837,8 @@ var setupMiddleware = async (c, next) => {
12817
12837
  const path = c.req.path;
12818
12838
  if (path.includes("/setup"))
12819
12839
  return next();
12840
+ if (!c.env?.DB)
12841
+ return await next();
12820
12842
  try {
12821
12843
  const db = createDb(c.env.DB);
12822
12844
  const setupComplete = await db.selectFrom("options").select("value").where("name", "=", "flare:setup_complete").executeTakeFirst();
@@ -42599,15 +42621,22 @@ contentRoutes.post("/:collection", async (c) => {
42599
42621
  const baseSlug = data.slug || data.title?.toLowerCase().replace(/[^a-z0-9]+/g, "-") || id;
42600
42622
  const slug = await ensureUniqueSlug(db, collectionName, baseSlug);
42601
42623
  const status = data.status || "draft";
42602
- const doc2 = {
42624
+ let doc2 = {
42603
42625
  ...data,
42604
42626
  id,
42605
42627
  slug,
42606
42628
  status
42607
42629
  };
42630
+ const pluginManager = c.get("pluginManager");
42631
+ if (pluginManager) {
42632
+ doc2 = await pluginManager.runContentBeforeSave(doc2, collectionName, true);
42633
+ }
42608
42634
  try {
42609
42635
  await db.insertInto(`ec_${collectionName}`).values(doc2).execute();
42610
- return apiResponse.created(c, { id, slug });
42636
+ if (pluginManager) {
42637
+ c.executionCtx.waitUntil(pluginManager.runContentAfterSave(doc2, collectionName, true));
42638
+ }
42639
+ return apiResponse.created(c, { id, slug: doc2.slug });
42611
42640
  } catch (e) {
42612
42641
  return apiResponse.error(c, `Failed query: ${e.message}`);
42613
42642
  }
@@ -42627,12 +42656,20 @@ contentRoutes.put("/:collection/:id", async (c) => {
42627
42656
  const uniqueSlug = await ensureUniqueSlug(db, collectionName, data.slug, id);
42628
42657
  finalData.slug = uniqueSlug;
42629
42658
  }
42659
+ const pluginManager = c.get("pluginManager");
42660
+ let docToSave = {
42661
+ ...finalData,
42662
+ updated_at: sql`CURRENT_TIMESTAMP`
42663
+ };
42664
+ if (pluginManager) {
42665
+ docToSave = await pluginManager.runContentBeforeSave(docToSave, collectionName, false);
42666
+ }
42630
42667
  try {
42631
- await db.updateTable(`ec_${collectionName}`).set({
42632
- ...finalData,
42633
- updated_at: sql`CURRENT_TIMESTAMP`
42634
- }).where("id", "=", id).execute();
42635
- return apiResponse.ok(c, { id, success: true, slug: finalData.slug });
42668
+ await db.updateTable(`ec_${collectionName}`).set(docToSave).where("id", "=", id).execute();
42669
+ if (pluginManager) {
42670
+ c.executionCtx.waitUntil(pluginManager.runContentAfterSave(docToSave, collectionName, false));
42671
+ }
42672
+ return apiResponse.ok(c, { id, success: true, slug: docToSave.slug });
42636
42673
  } catch (e) {
42637
42674
  return apiResponse.error(c, e.message);
42638
42675
  }
@@ -42641,8 +42678,17 @@ contentRoutes.delete("/:collection/:id", async (c) => {
42641
42678
  const collectionName = c.req.param("collection");
42642
42679
  const id = c.req.param("id");
42643
42680
  const db = createDb(c.env.DB);
42681
+ const pluginManager = c.get("pluginManager");
42682
+ if (pluginManager) {
42683
+ const allowed = await pluginManager.runContentBeforeDelete(id, collectionName);
42684
+ if (!allowed)
42685
+ return apiResponse.error(c, "Deletion prevented by plugin", 403);
42686
+ }
42644
42687
  try {
42645
42688
  await db.deleteFrom(`ec_${collectionName}`).where("id", "=", id).execute();
42689
+ if (pluginManager) {
42690
+ c.executionCtx.waitUntil(pluginManager.runContentAfterDelete(id, collectionName));
42691
+ }
42646
42692
  return apiResponse.ok(c, { success: true });
42647
42693
  } catch (e) {
42648
42694
  return apiResponse.error(c, e.message);
@@ -43133,15 +43179,23 @@ mcpRoutes.post("/execute", async (c) => {
43133
43179
  const baseSlug = docData.slug || docData.title?.toLowerCase().replace(/[^a-z0-9]+/g, "-") || id;
43134
43180
  const slug = await ensureUniqueSlug(db, collectionName, baseSlug);
43135
43181
  const status = docData.status || "draft";
43136
- const doc2 = {
43182
+ let doc2 = {
43137
43183
  ...docData,
43138
43184
  id,
43139
43185
  slug,
43140
43186
  status
43141
43187
  };
43188
+ const pluginManager = c.get("pluginManager");
43189
+ if (pluginManager) {
43190
+ const result = await pluginManager.runContentBeforeSave(doc2, collectionName, true);
43191
+ doc2 = { ...result, id, slug: result.slug || slug, status: result.status || status };
43192
+ }
43142
43193
  await db.insertInto(`ec_${collectionName}`).values(doc2).execute();
43194
+ if (pluginManager) {
43195
+ c.executionCtx.waitUntil(pluginManager.runContentAfterSave(doc2, collectionName, true));
43196
+ }
43143
43197
  return c.json({
43144
- content: [{ type: "text", text: `Success: Document created with ID ${id} and slug ${slug}` }]
43198
+ content: [{ type: "text", text: `Success: Document created with ID ${id} and slug ${doc2.slug}` }]
43145
43199
  });
43146
43200
  }
43147
43201
  if (tool === "update_document") {
@@ -43154,14 +43208,22 @@ mcpRoutes.post("/execute", async (c) => {
43154
43208
  const parsed = dynamicContentSchema.safeParse(data);
43155
43209
  if (!parsed.success)
43156
43210
  return c.json({ error: parsed.error.format() }, 400);
43157
- let finalData = { ...parsed.data };
43158
- if (finalData.slug) {
43159
- finalData.slug = await ensureUniqueSlug(db, collectionName, finalData.slug, id);
43160
- }
43161
- await db.updateTable(`ec_${collectionName}`).set({
43162
- ...finalData,
43211
+ const pluginManager = c.get("pluginManager");
43212
+ let docToSave = {
43213
+ ...parsed.data,
43163
43214
  updated_at: sql`CURRENT_TIMESTAMP`
43164
- }).where("id", "=", id).execute();
43215
+ };
43216
+ if (docToSave.slug) {
43217
+ docToSave.slug = await ensureUniqueSlug(db, collectionName, docToSave.slug, id);
43218
+ }
43219
+ if (pluginManager) {
43220
+ const result = await pluginManager.runContentBeforeSave(docToSave, collectionName, false);
43221
+ docToSave = { ...result, updated_at: result.updated_at || docToSave.updated_at };
43222
+ }
43223
+ await db.updateTable(`ec_${collectionName}`).set(docToSave).where("id", "=", id).execute();
43224
+ if (pluginManager) {
43225
+ c.executionCtx.waitUntil(pluginManager.runContentAfterSave(docToSave, collectionName, false));
43226
+ }
43165
43227
  return c.json({
43166
43228
  content: [{ type: "text", text: `Success: Document ${id} updated.` }]
43167
43229
  });
@@ -43239,11 +43301,541 @@ mcpRoutes.post("/execute", async (c) => {
43239
43301
  }
43240
43302
  });
43241
43303
 
43304
+ // src/api/routes/plugins.ts
43305
+ import { Hono as Hono11 } from "hono";
43306
+ var pluginRoutes = new Hono11;
43307
+ pluginRoutes.get("/", async (c) => {
43308
+ const manager = c.get("pluginManager");
43309
+ if (!manager)
43310
+ return apiResponse.error(c, "Plugin system not initialized");
43311
+ const plugins = manager.getActivePlugins();
43312
+ return apiResponse.ok(c, plugins.map((p) => ({
43313
+ id: p.id,
43314
+ name: p.name,
43315
+ version: p.version,
43316
+ capabilities: p.capabilities,
43317
+ routes: manager.getRoutes?.(p.id) ?? [],
43318
+ adminPages: p.adminPages ?? [],
43319
+ adminWidgets: p.adminWidgets ?? []
43320
+ })));
43321
+ });
43322
+ pluginRoutes.post("/:id/admin", async (c) => {
43323
+ const manager = c.get("pluginManager");
43324
+ if (!manager)
43325
+ return apiResponse.error(c, "Plugin system not initialized");
43326
+ const pluginId = c.req.param("id");
43327
+ const interaction = await c.req.json().catch(() => ({}));
43328
+ try {
43329
+ const result = await manager.invokeAdmin(pluginId, interaction);
43330
+ return apiResponse.ok(c, result);
43331
+ } catch (err) {
43332
+ return apiResponse.error(c, err.message, 400);
43333
+ }
43334
+ });
43335
+ pluginRoutes.post("/:id/routes/:name", async (c) => {
43336
+ const manager = c.get("pluginManager");
43337
+ if (!manager)
43338
+ return apiResponse.error(c, "Plugin system not initialized");
43339
+ const pluginId = c.req.param("id");
43340
+ const routeName = c.req.param("name");
43341
+ const input = await c.req.json().catch(() => ({}));
43342
+ const serializedReq = {
43343
+ url: c.req.url,
43344
+ method: c.req.method,
43345
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
43346
+ meta: {
43347
+ ip: c.req.header("x-real-ip") || c.req.header("cf-connecting-ip") || null,
43348
+ userAgent: c.req.header("user-agent") || null,
43349
+ referer: c.req.header("referer") || null
43350
+ }
43351
+ };
43352
+ try {
43353
+ const result = await manager.invokeRoute(pluginId, routeName, {
43354
+ input,
43355
+ request: serializedReq
43356
+ });
43357
+ return apiResponse.ok(c, result);
43358
+ } catch (err) {
43359
+ return apiResponse.error(c, err.message, 400);
43360
+ }
43361
+ });
43362
+
43363
+ // src/plugins/context.ts
43364
+ function createPluginContext(options) {
43365
+ const { pluginId, version: version2, capabilities, allowedHosts, storageCollections, db, siteInfo } = options;
43366
+ const hasCap = (cap) => capabilities.includes(cap);
43367
+ const log3 = {
43368
+ debug: (msg, data) => console.debug(`[${pluginId}] DEBUG: ${msg}`, data ?? ""),
43369
+ info: (msg, data) => console.info(`[${pluginId}] INFO: ${msg}`, data ?? ""),
43370
+ warn: (msg, data) => console.warn(`[${pluginId}] WARN: ${msg}`, data ?? ""),
43371
+ error: (msg, data) => console.error(`[${pluginId}] ERROR: ${msg}`, data ?? "")
43372
+ };
43373
+ const kv = {
43374
+ get: async (key) => {
43375
+ const res = await db.selectFrom("_fc_plugin_storage").select("data").where("plugin_id", "=", pluginId).where("collection", "=", "_kv").where("id", "=", key).executeTakeFirst();
43376
+ return res ? JSON.parse(res.data) : null;
43377
+ },
43378
+ set: async (key, value) => {
43379
+ await sql`
43380
+ INSERT INTO _fc_plugin_storage (plugin_id, collection, id, data)
43381
+ VALUES (${pluginId}, '_kv', ${key}, ${JSON.stringify(value)})
43382
+ ON CONFLICT(plugin_id, collection, id) DO UPDATE SET
43383
+ data = excluded.data,
43384
+ updated_at = CURRENT_TIMESTAMP
43385
+ `.execute(db);
43386
+ },
43387
+ delete: async (key) => {
43388
+ const res = await db.deleteFrom("_fc_plugin_storage").where("plugin_id", "=", pluginId).where("collection", "=", "_kv").where("id", "=", key).executeTakeFirst();
43389
+ return !!res;
43390
+ },
43391
+ list: async (prefix) => {
43392
+ let query = db.selectFrom("_fc_plugin_storage").select(["id", "data"]).where("plugin_id", "=", pluginId).where("collection", "=", "_kv");
43393
+ if (prefix) {
43394
+ query = query.where("id", "like", `${prefix}%`);
43395
+ }
43396
+ const results = await query.execute();
43397
+ return results.map((r) => ({
43398
+ key: r.id,
43399
+ value: JSON.parse(r.data)
43400
+ }));
43401
+ }
43402
+ };
43403
+ const storage = new Proxy({}, {
43404
+ get: (_, collection) => {
43405
+ if (!storageCollections.includes(collection)) {
43406
+ throw new Error(`Plugin "${pluginId}" attempted to access undeclared storage collection "${collection}".`);
43407
+ }
43408
+ return {
43409
+ get: async (id) => {
43410
+ const res = await db.selectFrom("_fc_plugin_storage").select("data").where("plugin_id", "=", pluginId).where("collection", "=", collection).where("id", "=", id).executeTakeFirst();
43411
+ return res ? JSON.parse(res.data) : null;
43412
+ },
43413
+ put: async (id, data) => {
43414
+ await sql`
43415
+ INSERT INTO _fc_plugin_storage (plugin_id, collection, id, data)
43416
+ VALUES (${pluginId}, ${collection}, ${id}, ${JSON.stringify(data)})
43417
+ ON CONFLICT(plugin_id, collection, id) DO UPDATE SET
43418
+ data = excluded.data,
43419
+ updated_at = CURRENT_TIMESTAMP
43420
+ `.execute(db);
43421
+ },
43422
+ delete: async (id) => {
43423
+ const res = await db.deleteFrom("_fc_plugin_storage").where("plugin_id", "=", pluginId).where("collection", "=", collection).where("id", "=", id).executeTakeFirst();
43424
+ return !!res;
43425
+ },
43426
+ query: async (opts) => {
43427
+ const limit = opts?.limit ?? 20;
43428
+ let query = db.selectFrom("_fc_plugin_storage").select(["id", "data"]).where("plugin_id", "=", pluginId).where("collection", "=", collection).limit(limit);
43429
+ const results = await query.execute();
43430
+ return {
43431
+ items: results.map((r) => JSON.parse(r.data)),
43432
+ hasMore: results.length === limit
43433
+ };
43434
+ },
43435
+ count: async () => {
43436
+ const res = await db.selectFrom("_fc_plugin_storage").select(db.fn.count("id").as("count")).where("plugin_id", "=", pluginId).where("collection", "=", collection).executeTakeFirst();
43437
+ return Number(res?.count ?? 0);
43438
+ }
43439
+ };
43440
+ }
43441
+ });
43442
+ let content;
43443
+ if (hasCap("read:content")) {
43444
+ content = {
43445
+ get: async (col, id) => {
43446
+ const res = await db.selectFrom(`ec_${col}`).selectAll().where("id", "=", id).executeTakeFirst();
43447
+ return res || null;
43448
+ },
43449
+ list: async (col, opts) => {
43450
+ const limit = opts?.limit ?? 20;
43451
+ const res = await db.selectFrom(`ec_${col}`).selectAll().where("status", "!=", "deleted").limit(limit).execute();
43452
+ return { items: res, hasMore: res.length === limit };
43453
+ },
43454
+ create: async (col, data) => {
43455
+ if (!hasCap("write:content"))
43456
+ throw new Error("Capability write:content required");
43457
+ return await db.insertInto(`ec_${col}`).values(data).execute();
43458
+ },
43459
+ update: async (col, id, data) => {
43460
+ if (!hasCap("write:content"))
43461
+ throw new Error("Capability write:content required");
43462
+ return await db.updateTable(`ec_${col}`).set(data).where("id", "=", id).execute();
43463
+ },
43464
+ delete: async (col, id) => {
43465
+ if (!hasCap("write:content"))
43466
+ throw new Error("Capability write:content required");
43467
+ return !!await db.deleteFrom(`ec_${col}`).where("id", "=", id).execute();
43468
+ }
43469
+ };
43470
+ }
43471
+ let http;
43472
+ if (hasCap("network:fetch")) {
43473
+ http = {
43474
+ fetch: async (url2, init) => {
43475
+ const target2 = new URL(url2);
43476
+ if (!hasCap("network:fetch:any") && !allowedHosts.includes(target2.hostname)) {
43477
+ throw new Error(`Host "${target2.hostname}" is not in the allowedHosts list for plugin "${pluginId}".`);
43478
+ }
43479
+ const response = await fetch(url2, init);
43480
+ return {
43481
+ status: response.status,
43482
+ ok: response.ok,
43483
+ headers: response.headers,
43484
+ text: () => response.text(),
43485
+ json: () => response.json()
43486
+ };
43487
+ }
43488
+ };
43489
+ }
43490
+ let users;
43491
+ if (hasCap("read:users")) {
43492
+ users = {
43493
+ get: async (id) => db.selectFrom("fc_users").selectAll().where("id", "=", id).executeTakeFirst(),
43494
+ list: async (opts) => {
43495
+ const items = await db.selectFrom("fc_users").selectAll().limit(opts?.limit ?? 20).execute();
43496
+ return { items };
43497
+ }
43498
+ };
43499
+ }
43500
+ let crypto3;
43501
+ if (hasCap("crypto:encrypt")) {
43502
+ crypto3 = {
43503
+ encrypt: async (text) => {
43504
+ if (!options.encryptionSecret)
43505
+ throw new Error("Encryption secret not configured");
43506
+ return await encryptText(text, options.encryptionSecret);
43507
+ },
43508
+ decrypt: async (ciphertext) => {
43509
+ if (!options.encryptionSecret)
43510
+ throw new Error("Encryption secret not configured");
43511
+ return await decryptText(ciphertext, options.encryptionSecret);
43512
+ }
43513
+ };
43514
+ }
43515
+ return {
43516
+ plugin: { id: pluginId, version: version2 },
43517
+ kv,
43518
+ storage,
43519
+ content,
43520
+ media: undefined,
43521
+ http,
43522
+ log: log3,
43523
+ site: siteInfo,
43524
+ users,
43525
+ email: undefined,
43526
+ crypto: crypto3
43527
+ };
43528
+ }
43529
+ async function encryptText(text, secret) {
43530
+ const enc = new TextEncoder;
43531
+ const key = await globalThis.crypto.subtle.importKey("raw", enc.encode(secret.padEnd(32, "0").slice(0, 32)), { name: "AES-GCM" }, false, ["encrypt"]);
43532
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
43533
+ const encrypted = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(text));
43534
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
43535
+ combined.set(iv);
43536
+ combined.set(new Uint8Array(encrypted), iv.length);
43537
+ return btoa(String.fromCharCode(...combined));
43538
+ }
43539
+ async function decryptText(ciphertext, secret) {
43540
+ const enc = new TextEncoder;
43541
+ const combined = new Uint8Array(atob(ciphertext).split("").map((c) => c.charCodeAt(0)));
43542
+ const iv = combined.slice(0, 12);
43543
+ const data = combined.slice(12);
43544
+ const key = await globalThis.crypto.subtle.importKey("raw", enc.encode(secret.padEnd(32, "0").slice(0, 32)), { name: "AES-GCM" }, false, ["decrypt"]);
43545
+ const decrypted = await globalThis.crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
43546
+ return new TextDecoder().decode(decrypted);
43547
+ }
43548
+
43549
+ // src/plugins/hooks.ts
43550
+ class HookPipeline {
43551
+ hooks = {};
43552
+ plugins = [];
43553
+ db;
43554
+ siteInfo;
43555
+ constructor(plugins, db, siteInfo) {
43556
+ this.plugins = plugins;
43557
+ this.db = db;
43558
+ this.siteInfo = siteInfo;
43559
+ for (const plugin of plugins) {
43560
+ for (const [name2, hook] of Object.entries(plugin.hooks)) {
43561
+ if (!this.hooks[name2])
43562
+ this.hooks[name2] = [];
43563
+ this.hooks[name2].push(hook);
43564
+ }
43565
+ }
43566
+ for (const name2 of Object.keys(this.hooks)) {
43567
+ this.hooks[name2]?.sort((a, b) => a.priority - b.priority);
43568
+ }
43569
+ }
43570
+ async runChain(hookName, initialData) {
43571
+ const hooks = this.hooks[hookName];
43572
+ if (!hooks || hooks.length === 0)
43573
+ return initialData;
43574
+ let currentData = initialData;
43575
+ for (const hook of hooks) {
43576
+ const ctx = this.createContextForHook(hook);
43577
+ currentData = await this.executeWithTimeout(() => hook.handler(currentData, ctx), hook.timeout, hook.pluginId, hookName);
43578
+ }
43579
+ return currentData;
43580
+ }
43581
+ async runParallel(hookName, event) {
43582
+ const hooks = this.hooks[hookName];
43583
+ if (!hooks || hooks.length === 0)
43584
+ return;
43585
+ const promises = hooks.map(async (hook) => {
43586
+ const ctx = this.createContextForHook(hook);
43587
+ try {
43588
+ await this.executeWithTimeout(() => hook.handler(event, ctx), hook.timeout, hook.pluginId, hookName);
43589
+ } catch (err) {
43590
+ console.error(`Hook "${hookName}" failed for plugin "${hook.pluginId}":`, err);
43591
+ }
43592
+ });
43593
+ await Promise.allSettled(promises);
43594
+ }
43595
+ async runVeto(hookName, event) {
43596
+ const hooks = this.hooks[hookName];
43597
+ if (!hooks || hooks.length === 0)
43598
+ return true;
43599
+ for (const hook of hooks) {
43600
+ const ctx = this.createContextForHook(hook);
43601
+ const result = await this.executeWithTimeout(() => hook.handler(event, ctx), hook.timeout, hook.pluginId, hookName);
43602
+ if (result === false)
43603
+ return false;
43604
+ }
43605
+ return true;
43606
+ }
43607
+ async runContentBeforeSave(content, collection, isNew) {
43608
+ return this.runChain("content:beforeSave", { content, collection, isNew }).then((res) => {
43609
+ const data = res;
43610
+ return data.content || res;
43611
+ });
43612
+ }
43613
+ async runContentAfterSave(content, collection, isNew) {
43614
+ return this.runParallel("content:afterSave", { content, collection, isNew });
43615
+ }
43616
+ async runContentBeforeDelete(id, collection) {
43617
+ return this.runVeto("content:beforeDelete", { id, collection });
43618
+ }
43619
+ async runContentAfterDelete(id, collection) {
43620
+ return this.runParallel("content:afterDelete", { id, collection });
43621
+ }
43622
+ createContextForHook(hook) {
43623
+ const plugin = this.plugins.find((p) => p.id === hook.pluginId);
43624
+ return createPluginContext({
43625
+ pluginId: hook.pluginId,
43626
+ version: plugin?.version || "0.0.0",
43627
+ capabilities: plugin?.capabilities || [],
43628
+ allowedHosts: plugin?.allowedHosts || [],
43629
+ storageCollections: plugin ? Object.keys(plugin.storage) : [],
43630
+ db: this.db,
43631
+ siteInfo: this.siteInfo
43632
+ });
43633
+ }
43634
+ async executeWithTimeout(fn, timeoutMs, pluginId, hookName) {
43635
+ return Promise.race([
43636
+ fn(),
43637
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Hook "${hookName}" from plugin "${pluginId}" timed out after ${timeoutMs}ms`)), timeoutMs))
43638
+ ]);
43639
+ }
43640
+ }
43641
+
43642
+ // src/plugins/routes.ts
43643
+ class PluginRouteRegistry {
43644
+ plugins = new Map;
43645
+ db;
43646
+ siteInfo;
43647
+ encryptionSecret;
43648
+ constructor(db, siteInfo, encryptionSecret) {
43649
+ this.db = db;
43650
+ this.siteInfo = siteInfo;
43651
+ this.encryptionSecret = encryptionSecret;
43652
+ }
43653
+ register(plugin) {
43654
+ this.plugins.set(plugin.id, plugin);
43655
+ }
43656
+ async invoke(pluginId, routeName, options) {
43657
+ const plugin = this.plugins.get(pluginId);
43658
+ if (!plugin) {
43659
+ throw new Error(`Plugin "${pluginId}" not found or has no routes registered.`);
43660
+ }
43661
+ const route = plugin.routes[routeName];
43662
+ if (!route) {
43663
+ throw new Error(`Route "${routeName}" not found in plugin "${pluginId}".`);
43664
+ }
43665
+ const ctx = createPluginContext({
43666
+ pluginId,
43667
+ version: plugin.version,
43668
+ capabilities: plugin.capabilities,
43669
+ allowedHosts: plugin.allowedHosts,
43670
+ storageCollections: Object.keys(plugin.storage),
43671
+ db: this.db,
43672
+ siteInfo: this.siteInfo,
43673
+ encryptionSecret: this.encryptionSecret
43674
+ });
43675
+ const routeCtx = {
43676
+ input: options.input,
43677
+ request: options.request,
43678
+ requestMeta: options.request.meta
43679
+ };
43680
+ return await route.handler(routeCtx, ctx);
43681
+ }
43682
+ getRoutes(pluginId) {
43683
+ const plugin = this.plugins.get(pluginId);
43684
+ return plugin ? Object.keys(plugin.routes) : [];
43685
+ }
43686
+ }
43687
+
43688
+ // src/plugins/manager.ts
43689
+ class PluginManager {
43690
+ plugins;
43691
+ db;
43692
+ siteInfo;
43693
+ encryptionSecret;
43694
+ hookPipeline;
43695
+ routeRegistry;
43696
+ constructor(plugins, db, siteInfo, encryptionSecret) {
43697
+ this.plugins = plugins;
43698
+ this.db = db;
43699
+ this.siteInfo = siteInfo;
43700
+ this.encryptionSecret = encryptionSecret;
43701
+ this.hookPipeline = new HookPipeline(plugins, db, siteInfo);
43702
+ this.routeRegistry = new PluginRouteRegistry(db, siteInfo, encryptionSecret);
43703
+ for (const plugin of plugins) {
43704
+ this.routeRegistry.register(plugin);
43705
+ }
43706
+ }
43707
+ async runContentBeforeSave(content, collection, isNew) {
43708
+ return this.hookPipeline.runContentBeforeSave(content, collection, isNew);
43709
+ }
43710
+ async runContentAfterSave(content, collection, isNew) {
43711
+ return this.hookPipeline.runContentAfterSave(content, collection, isNew);
43712
+ }
43713
+ async runContentBeforeDelete(id, collection) {
43714
+ return this.hookPipeline.runContentBeforeDelete(id, collection);
43715
+ }
43716
+ async runContentAfterDelete(id, collection) {
43717
+ return this.hookPipeline.runContentAfterDelete(id, collection);
43718
+ }
43719
+ async invokeRoute(pluginId, routeName, options) {
43720
+ return this.routeRegistry.invoke(pluginId, routeName, options);
43721
+ }
43722
+ async invokeAdmin(pluginId, interaction) {
43723
+ const plugin = this.plugins.find((p) => p.id === pluginId);
43724
+ if (!plugin)
43725
+ throw new Error(`Plugin "${pluginId}" not found.`);
43726
+ if (!plugin.admin)
43727
+ throw new Error(`Plugin "${pluginId}" does not support admin interactions.`);
43728
+ const ctx = createPluginContext({
43729
+ pluginId: plugin.id,
43730
+ version: plugin.version,
43731
+ capabilities: plugin.capabilities,
43732
+ allowedHosts: plugin.allowedHosts,
43733
+ storageCollections: Object.keys(plugin.storage),
43734
+ db: this.db,
43735
+ siteInfo: this.siteInfo,
43736
+ encryptionSecret: this.encryptionSecret
43737
+ });
43738
+ return plugin.admin.handler(interaction, ctx);
43739
+ }
43740
+ getActivePlugins() {
43741
+ return this.plugins;
43742
+ }
43743
+ isActive(pluginId) {
43744
+ return this.plugins.some((p) => p.id === pluginId);
43745
+ }
43746
+ }
43747
+
43748
+ // src/plugins/adapt-entry.ts
43749
+ function resolveHook(hook, pluginId) {
43750
+ if (typeof hook === "function") {
43751
+ return {
43752
+ priority: 100,
43753
+ timeout: 5000,
43754
+ handler: hook,
43755
+ pluginId
43756
+ };
43757
+ }
43758
+ return {
43759
+ priority: hook.priority ?? 100,
43760
+ timeout: hook.timeout ?? 5000,
43761
+ handler: hook.handler,
43762
+ pluginId
43763
+ };
43764
+ }
43765
+ function resolveRoute(route) {
43766
+ return {
43767
+ input: route.input,
43768
+ public: route.public ?? false,
43769
+ handler: route.handler
43770
+ };
43771
+ }
43772
+ function adaptEntry(input) {
43773
+ const definition = input;
43774
+ const descriptor = input;
43775
+ const id = descriptor.id;
43776
+ const version2 = descriptor.version;
43777
+ const hooks = {};
43778
+ if (definition.hooks) {
43779
+ for (const [name2, entry] of Object.entries(definition.hooks)) {
43780
+ hooks[name2] = resolveHook(entry, id);
43781
+ }
43782
+ }
43783
+ const routes = {};
43784
+ if (definition.routes) {
43785
+ for (const [name2, entry] of Object.entries(definition.routes)) {
43786
+ routes[name2] = resolveRoute(entry);
43787
+ }
43788
+ }
43789
+ const storage = {};
43790
+ if (descriptor.storage) {
43791
+ for (const [name2, config2] of Object.entries(descriptor.storage)) {
43792
+ storage[name2] = {
43793
+ indexes: config2.indexes ?? []
43794
+ };
43795
+ }
43796
+ }
43797
+ return {
43798
+ id,
43799
+ name: descriptor.name || id,
43800
+ version: version2,
43801
+ capabilities: descriptor.capabilities || definition.capabilities || [],
43802
+ allowedHosts: descriptor.allowedHosts || definition.allowedHosts || [],
43803
+ storage,
43804
+ hooks,
43805
+ routes,
43806
+ admin: definition.admin,
43807
+ adminPages: descriptor.adminPages ?? [],
43808
+ adminWidgets: descriptor.adminWidgets ?? []
43809
+ };
43810
+ }
43811
+
43812
+ // src/plugins/middleware.ts
43813
+ function pluginMiddleware(staticPlugins = []) {
43814
+ return async (c, next) => {
43815
+ const resolvedPlugins = staticPlugins.map((p) => {
43816
+ return adaptEntry(p);
43817
+ });
43818
+ if (!c.env?.DB)
43819
+ return await next();
43820
+ const db = createDb(c.env.DB);
43821
+ const siteInfo = {
43822
+ name: "FlareCMS",
43823
+ url: new URL(c.req.url).origin,
43824
+ locale: "en"
43825
+ };
43826
+ const encryptionSecret = c.env.FLARE_ENCRYPTION_SECRET || c.env.AUTH_SECRET;
43827
+ const manager = new PluginManager(resolvedPlugins, db, siteInfo, encryptionSecret);
43828
+ c.set("pluginManager", manager);
43829
+ await next();
43830
+ };
43831
+ }
43832
+
43242
43833
  // src/server/index.ts
43243
43834
  function createFlareAPI(options = {}) {
43244
43835
  const base = options.base || "/admin";
43245
- const api2 = new Hono11;
43836
+ const api2 = new Hono12;
43246
43837
  api2.use("*", corsMiddleware);
43838
+ api2.use("*", pluginMiddleware(options.plugins));
43247
43839
  api2.use("*", async (c, next) => {
43248
43840
  const adminPrefix = base.replace(/^\//, "") || "admin";
43249
43841
  c.set("reservedSlugs", [
@@ -43260,6 +43852,7 @@ function createFlareAPI(options = {}) {
43260
43852
  ]);
43261
43853
  await next();
43262
43854
  });
43855
+ api2.get("/health", (c) => apiResponse.ok(c, { status: "ok" }));
43263
43856
  api2.use("/*", setupMiddleware);
43264
43857
  api2.use("/*", authMiddleware);
43265
43858
  api2.route("/auth", authRoutes);
@@ -43272,7 +43865,7 @@ function createFlareAPI(options = {}) {
43272
43865
  api2.route("/oauth", oauthRoutes);
43273
43866
  api2.route("/settings", settingsRoutes);
43274
43867
  api2.route("/mcp", mcpRoutes);
43275
- api2.get("/health", (c) => apiResponse.ok(c, { status: "ok" }));
43868
+ api2.route("/plugins", pluginRoutes);
43276
43869
  return api2;
43277
43870
  }
43278
43871
  export {