flarecms 0.1.2 → 0.2.1
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.
- package/LICENSE +20 -20
- package/README.md +73 -73
- package/dist/cli/commands.js +3377 -2603
- package/dist/cli/index.js +3378 -2603
- package/dist/client/index.js +64717 -6395
- package/dist/db/index.js +19 -1
- package/dist/index.js +65752 -6806
- package/dist/server/index.js +613 -20
- package/dist/style.css +163 -7
- package/dist/style.css.d.ts +8 -8
- package/package.json +27 -34
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42633
|
-
|
|
42634
|
-
}
|
|
42635
|
-
return apiResponse.ok(c, { id, success: true, 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
|
-
|
|
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
|
-
|
|
43158
|
-
|
|
43159
|
-
|
|
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
|
-
}
|
|
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
|
|
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.
|
|
43868
|
+
api2.route("/plugins", pluginRoutes);
|
|
43276
43869
|
return api2;
|
|
43277
43870
|
}
|
|
43278
43871
|
export {
|