emdash 0.1.0 → 0.1.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 +9 -0
- package/dist/{apply-Bjfq_b4-.mjs → apply-kC39ev1Z.mjs} +4 -4
- package/dist/{apply-Bjfq_b4-.mjs.map → apply-kC39ev1Z.mjs.map} +1 -1
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.mjs +16 -1
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/request-context.mjs +84 -22
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware.mjs +41 -12
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +5 -4
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/cli/index.mjs +65 -6
- package/dist/cli/index.mjs.map +1 -1
- package/dist/db/index.mjs +1 -1
- package/dist/{index-C1xF3OGh.d.mts → index-CLBc4gw-.d.mts} +42 -11
- package/dist/{index-C1xF3OGh.d.mts.map → index-CLBc4gw-.d.mts.map} +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +9 -9
- package/dist/{manifest-schema-Dcl0R6nM.mjs → manifest-schema-CL8DWO9b.mjs} +5 -2
- package/dist/manifest-schema-CL8DWO9b.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +4 -4
- package/dist/page/index.d.mts +1 -1
- package/dist/{placeholder-CmGAmqeO.d.mts → placeholder-SvFCKbz_.d.mts} +10 -2
- package/dist/{placeholder-CmGAmqeO.d.mts.map → placeholder-SvFCKbz_.d.mts.map} +1 -1
- package/dist/{placeholder-SmpOx-_v.mjs → placeholder-aiCD8aSZ.mjs} +27 -2
- package/dist/placeholder-aiCD8aSZ.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-CS_iSj34.mjs → query-BVYN0PJ6.mjs} +2 -2
- package/dist/{query-CS_iSj34.mjs.map → query-BVYN0PJ6.mjs.map} +1 -1
- package/dist/{registry-D_w5HW4G.mjs → registry-BNYQKX_d.mjs} +23 -38
- package/dist/registry-BNYQKX_d.mjs.map +1 -0
- package/dist/{runner-C0hCbYnD.mjs → runner-BraqvGYk.mjs} +251 -158
- package/dist/runner-BraqvGYk.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -1
- package/dist/runtime.d.mts +4 -4
- package/dist/{search-DG603UrT.mjs → search-C1gg67nN.mjs} +125 -18
- package/dist/search-C1gg67nN.mjs.map +1 -0
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +3 -3
- package/dist/{types-DvhsUmSJ.d.mts → types-BQo5JS0J.d.mts} +15 -2
- package/dist/{types-DvhsUmSJ.d.mts.map → types-BQo5JS0J.d.mts.map} +1 -1
- package/dist/{types-DY5zk5HN.mjs → types-CiA5Gac0.mjs} +5 -3
- package/dist/types-CiA5Gac0.mjs.map +1 -0
- package/dist/{types-C4-fAxN3.d.mts → types-DPfzHnjW.d.mts} +13 -2
- package/dist/types-DPfzHnjW.d.mts.map +1 -0
- package/dist/{validate-CpBtVMsD.d.mts → validate-HtxZeaBi.d.mts} +2 -2
- package/dist/{validate-CpBtVMsD.d.mts.map → validate-HtxZeaBi.d.mts.map} +1 -1
- package/dist/{validate-O7PWmlnq.mjs → validate-_rsF-Dx_.mjs} +2 -2
- package/dist/{validate-O7PWmlnq.mjs.map → validate-_rsF-Dx_.mjs.map} +1 -1
- package/package.json +6 -4
- package/src/api/handlers/marketplace.ts +7 -4
- package/src/api/schemas/schema.ts +12 -0
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/runtime.ts +13 -0
- package/src/astro/integration/virtual-modules.ts +13 -1
- package/src/astro/routes/admin.astro +1 -1
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +3 -1
- package/src/astro/routes/api/auth/invite/complete.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/register/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +2 -1
- package/src/astro/routes/api/auth/signup/complete.ts +2 -1
- package/src/astro/routes/api/import/wordpress/analyze.ts +24 -3
- package/src/astro/routes/api/import/wordpress/execute.ts +5 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +2 -2
- package/src/astro/routes/api/media.ts +16 -4
- package/src/astro/routes/api/search/index.ts +1 -5
- package/src/astro/routes/api/search/suggest.ts +1 -5
- package/src/astro/routes/api/setup/admin-verify.ts +2 -1
- package/src/astro/routes/api/setup/admin.ts +2 -1
- package/src/astro/types.ts +1 -0
- package/src/auth/passkey-config.ts +24 -3
- package/src/cli/commands/bundle-utils.ts +26 -0
- package/src/cli/commands/bundle.ts +15 -0
- package/src/cli/commands/content.ts +11 -1
- package/src/cli/commands/login.ts +2 -0
- package/src/cli/commands/media.ts +5 -1
- package/src/cli/commands/menu.ts +3 -1
- package/src/cli/commands/schema.ts +7 -1
- package/src/cli/commands/search-cmd.ts +2 -1
- package/src/cli/commands/taxonomy.ts +4 -1
- package/src/cli/output.ts +14 -0
- package/src/components/InlinePortableTextEditor.tsx +33 -3
- package/src/database/migrations/033_optimize_content_indexes.ts +113 -0
- package/src/database/migrations/runner.ts +40 -33
- package/src/database/repositories/comment.ts +32 -20
- package/src/emdash-runtime.ts +64 -2
- package/src/media/placeholder.ts +31 -0
- package/src/media/thumbnail.ts +32 -0
- package/src/plugins/hooks.ts +91 -0
- package/src/plugins/manager.ts +22 -0
- package/src/plugins/manifest-schema.ts +3 -0
- package/src/plugins/marketplace.ts +25 -12
- package/src/plugins/types.ts +24 -0
- package/src/schema/registry.ts +23 -27
- package/src/schema/types.ts +27 -1
- package/src/search/fts-manager.ts +1 -18
- package/src/visual-editing/toolbar.ts +84 -22
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +0 -1
- package/dist/placeholder-SmpOx-_v.mjs.map +0 -1
- package/dist/registry-D_w5HW4G.mjs.map +0 -1
- package/dist/runner-C0hCbYnD.mjs.map +0 -1
- package/dist/search-DG603UrT.mjs.map +0 -1
- package/dist/types-C4-fAxN3.d.mts.map +0 -1
- package/dist/types-DY5zk5HN.mjs.map +0 -1
package/src/plugins/hooks.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
PluginContext,
|
|
18
18
|
ContentHookEvent,
|
|
19
19
|
ContentDeleteEvent,
|
|
20
|
+
ContentPublishStateChangeEvent,
|
|
20
21
|
MediaUploadEvent,
|
|
21
22
|
MediaAfterUploadEvent,
|
|
22
23
|
LifecycleEvent,
|
|
@@ -30,6 +31,8 @@ import type {
|
|
|
30
31
|
ContentAfterSaveHandler,
|
|
31
32
|
ContentBeforeDeleteHandler,
|
|
32
33
|
ContentAfterDeleteHandler,
|
|
34
|
+
ContentAfterPublishHandler,
|
|
35
|
+
ContentAfterUnpublishHandler,
|
|
33
36
|
MediaBeforeUploadHandler,
|
|
34
37
|
MediaAfterUploadHandler,
|
|
35
38
|
LifecycleHandler,
|
|
@@ -61,6 +64,8 @@ type HookNameV2 =
|
|
|
61
64
|
| "content:afterSave"
|
|
62
65
|
| "content:beforeDelete"
|
|
63
66
|
| "content:afterDelete"
|
|
67
|
+
| "content:afterPublish"
|
|
68
|
+
| "content:afterUnpublish"
|
|
64
69
|
| "media:beforeUpload"
|
|
65
70
|
| "media:afterUpload"
|
|
66
71
|
| "cron"
|
|
@@ -86,6 +91,8 @@ interface HookHandlerMap {
|
|
|
86
91
|
"content:afterSave": ContentAfterSaveHandler;
|
|
87
92
|
"content:beforeDelete": ContentBeforeDeleteHandler;
|
|
88
93
|
"content:afterDelete": ContentAfterDeleteHandler;
|
|
94
|
+
"content:afterPublish": ContentAfterPublishHandler;
|
|
95
|
+
"content:afterUnpublish": ContentAfterUnpublishHandler;
|
|
89
96
|
"media:beforeUpload": MediaBeforeUploadHandler;
|
|
90
97
|
"media:afterUpload": MediaAfterUploadHandler;
|
|
91
98
|
cron: CronHandler;
|
|
@@ -211,6 +218,8 @@ export class HookPipeline {
|
|
|
211
218
|
this.registerPluginHook(plugin, "content:afterSave");
|
|
212
219
|
this.registerPluginHook(plugin, "content:beforeDelete");
|
|
213
220
|
this.registerPluginHook(plugin, "content:afterDelete");
|
|
221
|
+
this.registerPluginHook(plugin, "content:afterPublish");
|
|
222
|
+
this.registerPluginHook(plugin, "content:afterUnpublish");
|
|
214
223
|
this.registerPluginHook(plugin, "media:beforeUpload");
|
|
215
224
|
this.registerPluginHook(plugin, "media:afterUpload");
|
|
216
225
|
this.registerPluginHook(plugin, "cron");
|
|
@@ -249,6 +258,8 @@ export class HookPipeline {
|
|
|
249
258
|
["content:afterSave", "read:content"],
|
|
250
259
|
["content:beforeDelete", "read:content"],
|
|
251
260
|
["content:afterDelete", "read:content"],
|
|
261
|
+
["content:afterPublish", "read:content"],
|
|
262
|
+
["content:afterUnpublish", "read:content"],
|
|
252
263
|
// Media
|
|
253
264
|
["media:beforeUpload", "write:media"],
|
|
254
265
|
["media:afterUpload", "read:media"],
|
|
@@ -620,6 +631,86 @@ export class HookPipeline {
|
|
|
620
631
|
return results;
|
|
621
632
|
}
|
|
622
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Run content:afterPublish hooks (fire-and-forget).
|
|
636
|
+
*/
|
|
637
|
+
async runContentAfterPublish(
|
|
638
|
+
content: Record<string, unknown>,
|
|
639
|
+
collection: string,
|
|
640
|
+
): Promise<HookResult<void>[]> {
|
|
641
|
+
const hooks = this.getTypedHooks("content:afterPublish");
|
|
642
|
+
const results: HookResult<void>[] = [];
|
|
643
|
+
|
|
644
|
+
for (const hook of hooks) {
|
|
645
|
+
const { handler } = hook;
|
|
646
|
+
const event: ContentPublishStateChangeEvent = { content, collection };
|
|
647
|
+
const ctx = this.getContext(hook.pluginId);
|
|
648
|
+
const start = Date.now();
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
|
|
652
|
+
results.push({
|
|
653
|
+
success: true,
|
|
654
|
+
pluginId: hook.pluginId,
|
|
655
|
+
duration: Date.now() - start,
|
|
656
|
+
});
|
|
657
|
+
} catch (error) {
|
|
658
|
+
results.push({
|
|
659
|
+
success: false,
|
|
660
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
661
|
+
pluginId: hook.pluginId,
|
|
662
|
+
duration: Date.now() - start,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (hook.errorPolicy === "abort") {
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return results;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Run content:afterUnpublish hooks (fire-and-forget).
|
|
676
|
+
*/
|
|
677
|
+
async runContentAfterUnpublish(
|
|
678
|
+
content: Record<string, unknown>,
|
|
679
|
+
collection: string,
|
|
680
|
+
): Promise<HookResult<void>[]> {
|
|
681
|
+
const hooks = this.getTypedHooks("content:afterUnpublish");
|
|
682
|
+
const results: HookResult<void>[] = [];
|
|
683
|
+
|
|
684
|
+
for (const hook of hooks) {
|
|
685
|
+
const { handler } = hook;
|
|
686
|
+
const event: ContentPublishStateChangeEvent = { content, collection };
|
|
687
|
+
const ctx = this.getContext(hook.pluginId);
|
|
688
|
+
const start = Date.now();
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);
|
|
692
|
+
results.push({
|
|
693
|
+
success: true,
|
|
694
|
+
pluginId: hook.pluginId,
|
|
695
|
+
duration: Date.now() - start,
|
|
696
|
+
});
|
|
697
|
+
} catch (error) {
|
|
698
|
+
results.push({
|
|
699
|
+
success: false,
|
|
700
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
701
|
+
pluginId: hook.pluginId,
|
|
702
|
+
duration: Date.now() - start,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
if (hook.errorPolicy === "abort") {
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return results;
|
|
712
|
+
}
|
|
713
|
+
|
|
623
714
|
// =========================================================================
|
|
624
715
|
// Media Hooks
|
|
625
716
|
// =========================================================================
|
package/src/plugins/manager.ts
CHANGED
|
@@ -326,6 +326,28 @@ export class PluginManager {
|
|
|
326
326
|
return this.hookPipeline!.runContentAfterDelete(id, collection);
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Run content:afterPublish hooks across all active plugins
|
|
331
|
+
*/
|
|
332
|
+
async runContentAfterPublish(
|
|
333
|
+
content: Record<string, unknown>,
|
|
334
|
+
collection: string,
|
|
335
|
+
): Promise<HookResult<void>[]> {
|
|
336
|
+
this.ensureInitialized();
|
|
337
|
+
return this.hookPipeline!.runContentAfterPublish(content, collection);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Run content:afterUnpublish hooks across all active plugins
|
|
342
|
+
*/
|
|
343
|
+
async runContentAfterUnpublish(
|
|
344
|
+
content: Record<string, unknown>,
|
|
345
|
+
collection: string,
|
|
346
|
+
): Promise<HookResult<void>[]> {
|
|
347
|
+
this.ensureInitialized();
|
|
348
|
+
return this.hookPipeline!.runContentAfterUnpublish(content, collection);
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
/**
|
|
330
352
|
* Run media:beforeUpload hooks across all active plugins
|
|
331
353
|
*/
|
|
@@ -42,6 +42,7 @@ const FIELD_TYPES = [
|
|
|
42
42
|
"reference",
|
|
43
43
|
"json",
|
|
44
44
|
"slug",
|
|
45
|
+
"repeater",
|
|
45
46
|
] as const;
|
|
46
47
|
|
|
47
48
|
export const HOOK_NAMES = [
|
|
@@ -53,6 +54,8 @@ export const HOOK_NAMES = [
|
|
|
53
54
|
"content:afterSave",
|
|
54
55
|
"content:beforeDelete",
|
|
55
56
|
"content:afterDelete",
|
|
57
|
+
"content:afterPublish",
|
|
58
|
+
"content:afterUnpublish",
|
|
56
59
|
"media:beforeUpload",
|
|
57
60
|
"media:afterUpload",
|
|
58
61
|
"cron",
|
|
@@ -204,10 +204,12 @@ export class MarketplaceUnavailableError extends MarketplaceError {
|
|
|
204
204
|
|
|
205
205
|
class MarketplaceClientImpl implements MarketplaceClient {
|
|
206
206
|
private readonly baseUrl: string;
|
|
207
|
+
private readonly siteOrigin: string | undefined;
|
|
207
208
|
|
|
208
|
-
constructor(baseUrl: string) {
|
|
209
|
+
constructor(baseUrl: string, siteOrigin?: string) {
|
|
209
210
|
// Strip trailing slash
|
|
210
211
|
this.baseUrl = baseUrl.replace(TRAILING_SLASHES, "");
|
|
212
|
+
this.siteOrigin = siteOrigin;
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
async search(query?: string, opts?: MarketplaceSearchOpts): Promise<MarketplaceSearchResult> {
|
|
@@ -270,8 +272,8 @@ class MarketplaceClientImpl implements MarketplaceClient {
|
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
async reportInstall(id: string, version: string): Promise<void> {
|
|
273
|
-
// Generate a stable site hash (best-effort, non-identifying)
|
|
274
|
-
const siteHash = await generateSiteHash();
|
|
275
|
+
// Generate a stable site hash from the site origin (best-effort, non-identifying)
|
|
276
|
+
const siteHash = await generateSiteHash(this.siteOrigin);
|
|
275
277
|
const url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/installs`;
|
|
276
278
|
|
|
277
279
|
try {
|
|
@@ -433,18 +435,27 @@ async function extractBundle(tarballBytes: Uint8Array): Promise<PluginBundle> {
|
|
|
433
435
|
|
|
434
436
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
435
437
|
|
|
436
|
-
/**
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Generate a stable non-identifying site hash from the site origin.
|
|
440
|
+
* The same origin always produces the same hash, so the marketplace
|
|
441
|
+
* installs table deduplicates correctly per (plugin_id, site_hash).
|
|
442
|
+
*/
|
|
443
|
+
async function generateSiteHash(siteOrigin?: string): Promise<string> {
|
|
444
|
+
const seed = siteOrigin ? `emdash-site:${siteOrigin}` : `emdash-anonymous`;
|
|
441
445
|
try {
|
|
442
446
|
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed));
|
|
443
447
|
const arr = new Uint8Array(hash);
|
|
444
448
|
return Array.from(arr.slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
445
449
|
} catch {
|
|
446
|
-
// Fallback for environments without crypto.subtle
|
|
447
|
-
|
|
450
|
+
// Fallback for environments without crypto.subtle: FNV-1a hash encoded as hex.
|
|
451
|
+
// Deterministic, uniform distribution, no origin leakage.
|
|
452
|
+
let h = 0x811c9dc5;
|
|
453
|
+
for (let i = 0; i < seed.length; i++) {
|
|
454
|
+
h ^= seed.charCodeAt(i);
|
|
455
|
+
h = Math.imul(h, 0x01000193);
|
|
456
|
+
}
|
|
457
|
+
const h2 = h ^ (h >>> 16);
|
|
458
|
+
return (h >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
448
459
|
}
|
|
449
460
|
}
|
|
450
461
|
|
|
@@ -454,7 +465,9 @@ async function generateSiteHash(): Promise<string> {
|
|
|
454
465
|
* Create a MarketplaceClient for the given marketplace URL.
|
|
455
466
|
*
|
|
456
467
|
* @param baseUrl - The marketplace API base URL (e.g. "https://marketplace.emdashcms.com")
|
|
468
|
+
* @param siteOrigin - The origin of the EmDash site (e.g. "https://myblog.example.com").
|
|
469
|
+
* Used to generate a stable, non-identifying site hash for install deduplication.
|
|
457
470
|
*/
|
|
458
|
-
export function createMarketplaceClient(baseUrl: string): MarketplaceClient {
|
|
459
|
-
return new MarketplaceClientImpl(baseUrl);
|
|
471
|
+
export function createMarketplaceClient(baseUrl: string, siteOrigin?: string): MarketplaceClient {
|
|
472
|
+
return new MarketplaceClientImpl(baseUrl, siteOrigin);
|
|
460
473
|
}
|
package/src/plugins/types.ts
CHANGED
|
@@ -659,6 +659,14 @@ export interface ContentDeleteEvent {
|
|
|
659
659
|
collection: string;
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
/**
|
|
663
|
+
* Content publish state change hook event (fired after publish or unpublish)
|
|
664
|
+
*/
|
|
665
|
+
export interface ContentPublishStateChangeEvent {
|
|
666
|
+
content: Record<string, unknown>;
|
|
667
|
+
collection: string;
|
|
668
|
+
}
|
|
669
|
+
|
|
662
670
|
/**
|
|
663
671
|
* Media hook event
|
|
664
672
|
*/
|
|
@@ -708,6 +716,16 @@ export type ContentAfterDeleteHandler = (
|
|
|
708
716
|
ctx: PluginContext,
|
|
709
717
|
) => Promise<void>;
|
|
710
718
|
|
|
719
|
+
export type ContentAfterPublishHandler = (
|
|
720
|
+
event: ContentPublishStateChangeEvent,
|
|
721
|
+
ctx: PluginContext,
|
|
722
|
+
) => Promise<void>;
|
|
723
|
+
|
|
724
|
+
export type ContentAfterUnpublishHandler = (
|
|
725
|
+
event: ContentPublishStateChangeEvent,
|
|
726
|
+
ctx: PluginContext,
|
|
727
|
+
) => Promise<void>;
|
|
728
|
+
|
|
711
729
|
export type MediaBeforeUploadHandler = (
|
|
712
730
|
event: MediaUploadEvent,
|
|
713
731
|
ctx: PluginContext,
|
|
@@ -857,6 +875,10 @@ export interface PluginHooks {
|
|
|
857
875
|
"content:afterSave"?: HookConfig<ContentAfterSaveHandler> | ContentAfterSaveHandler;
|
|
858
876
|
"content:beforeDelete"?: HookConfig<ContentBeforeDeleteHandler> | ContentBeforeDeleteHandler;
|
|
859
877
|
"content:afterDelete"?: HookConfig<ContentAfterDeleteHandler> | ContentAfterDeleteHandler;
|
|
878
|
+
"content:afterPublish"?: HookConfig<ContentAfterPublishHandler> | ContentAfterPublishHandler;
|
|
879
|
+
"content:afterUnpublish"?:
|
|
880
|
+
| HookConfig<ContentAfterUnpublishHandler>
|
|
881
|
+
| ContentAfterUnpublishHandler;
|
|
860
882
|
|
|
861
883
|
// Media hooks
|
|
862
884
|
"media:beforeUpload"?: HookConfig<MediaBeforeUploadHandler> | MediaBeforeUploadHandler;
|
|
@@ -1157,6 +1179,8 @@ export interface ResolvedPluginHooks {
|
|
|
1157
1179
|
"content:afterSave"?: ResolvedHook<ContentAfterSaveHandler>;
|
|
1158
1180
|
"content:beforeDelete"?: ResolvedHook<ContentBeforeDeleteHandler>;
|
|
1159
1181
|
"content:afterDelete"?: ResolvedHook<ContentAfterDeleteHandler>;
|
|
1182
|
+
"content:afterPublish"?: ResolvedHook<ContentAfterPublishHandler>;
|
|
1183
|
+
"content:afterUnpublish"?: ResolvedHook<ContentAfterUnpublishHandler>;
|
|
1160
1184
|
"media:beforeUpload"?: ResolvedHook<MediaBeforeUploadHandler>;
|
|
1161
1185
|
"media:afterUpload"?: ResolvedHook<MediaAfterUploadHandler>;
|
|
1162
1186
|
cron?: ResolvedHook<CronHandler>;
|
package/src/schema/registry.ts
CHANGED
|
@@ -540,64 +540,60 @@ export class SchemaRegistry {
|
|
|
540
540
|
|
|
541
541
|
// Create standard indexes
|
|
542
542
|
await sql`
|
|
543
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}
|
|
544
|
-
ON ${sql.ref(tableName)} (status)
|
|
545
|
-
`.execute(conn);
|
|
546
|
-
|
|
547
|
-
await sql`
|
|
548
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_slug`)}
|
|
543
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_slug`)}
|
|
549
544
|
ON ${sql.ref(tableName)} (slug)
|
|
550
545
|
`.execute(conn);
|
|
551
546
|
|
|
552
547
|
await sql`
|
|
553
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}
|
|
554
|
-
ON ${sql.ref(tableName)} (created_at)
|
|
555
|
-
`.execute(conn);
|
|
556
|
-
|
|
557
|
-
await sql`
|
|
558
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_deleted`)}
|
|
559
|
-
ON ${sql.ref(tableName)} (deleted_at)
|
|
560
|
-
`.execute(conn);
|
|
561
|
-
|
|
562
|
-
await sql`
|
|
563
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_scheduled`)}
|
|
548
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_scheduled`)}
|
|
564
549
|
ON ${sql.ref(tableName)} (scheduled_at)
|
|
565
550
|
WHERE scheduled_at IS NOT NULL
|
|
566
551
|
`.execute(conn);
|
|
567
552
|
|
|
568
553
|
await sql`
|
|
569
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_live_revision`)}
|
|
554
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_live_revision`)}
|
|
570
555
|
ON ${sql.ref(tableName)} (live_revision_id)
|
|
571
556
|
`.execute(conn);
|
|
572
557
|
|
|
573
558
|
await sql`
|
|
574
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_draft_revision`)}
|
|
559
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_draft_revision`)}
|
|
575
560
|
ON ${sql.ref(tableName)} (draft_revision_id)
|
|
576
561
|
`.execute(conn);
|
|
577
562
|
|
|
578
563
|
await sql`
|
|
579
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_author`)}
|
|
564
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_author`)}
|
|
580
565
|
ON ${sql.ref(tableName)} (author_id)
|
|
581
566
|
`.execute(conn);
|
|
582
567
|
|
|
583
568
|
await sql`
|
|
584
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}
|
|
569
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}
|
|
585
570
|
ON ${sql.ref(tableName)} (primary_byline_id)
|
|
586
571
|
`.execute(conn);
|
|
587
572
|
|
|
588
573
|
await sql`
|
|
589
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}
|
|
590
|
-
ON ${sql.ref(tableName)} (
|
|
574
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_locale`)}
|
|
575
|
+
ON ${sql.ref(tableName)} (locale)
|
|
591
576
|
`.execute(conn);
|
|
592
577
|
|
|
593
578
|
await sql`
|
|
594
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}
|
|
595
|
-
ON ${sql.ref(tableName)} (
|
|
579
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_translation_group`)}
|
|
580
|
+
ON ${sql.ref(tableName)} (translation_group)
|
|
596
581
|
`.execute(conn);
|
|
597
582
|
|
|
583
|
+
// Composite indexes for optimized query performance (see migration 033)
|
|
598
584
|
await sql`
|
|
599
|
-
CREATE INDEX ${sql.ref(`idx_${tableName}
|
|
600
|
-
ON ${sql.ref(tableName)} (
|
|
585
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_deleted_updated_id`)}
|
|
586
|
+
ON ${sql.ref(tableName)} (deleted_at, updated_at DESC, id DESC)
|
|
587
|
+
`.execute(conn);
|
|
588
|
+
|
|
589
|
+
await sql`
|
|
590
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_deleted_status`)}
|
|
591
|
+
ON ${sql.ref(tableName)} (deleted_at, status)
|
|
592
|
+
`.execute(conn);
|
|
593
|
+
|
|
594
|
+
await sql`
|
|
595
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_deleted_created_id`)}
|
|
596
|
+
ON ${sql.ref(tableName)} (deleted_at, created_at DESC, id DESC)
|
|
601
597
|
`.execute(conn);
|
|
602
598
|
}
|
|
603
599
|
|
package/src/schema/types.ts
CHANGED
|
@@ -22,7 +22,8 @@ export type FieldType =
|
|
|
22
22
|
| "file"
|
|
23
23
|
| "reference"
|
|
24
24
|
| "json"
|
|
25
|
-
| "slug"
|
|
25
|
+
| "slug"
|
|
26
|
+
| "repeater";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Array of all field types for validation
|
|
@@ -42,6 +43,7 @@ export const FIELD_TYPES: readonly FieldType[] = [
|
|
|
42
43
|
"reference",
|
|
43
44
|
"json",
|
|
44
45
|
"slug",
|
|
46
|
+
"repeater",
|
|
45
47
|
] as const;
|
|
46
48
|
|
|
47
49
|
/**
|
|
@@ -67,6 +69,7 @@ export const FIELD_TYPE_TO_COLUMN: Record<FieldType, ColumnType> = {
|
|
|
67
69
|
reference: "TEXT",
|
|
68
70
|
json: "JSON",
|
|
69
71
|
slug: "TEXT",
|
|
72
|
+
repeater: "JSON",
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
/**
|
|
@@ -93,6 +96,26 @@ export type CollectionSource =
|
|
|
93
96
|
/**
|
|
94
97
|
* Validation rules for a field
|
|
95
98
|
*/
|
|
99
|
+
/** Sub-field definition for repeater fields */
|
|
100
|
+
export interface RepeaterSubField {
|
|
101
|
+
slug: string;
|
|
102
|
+
type: "string" | "text" | "number" | "integer" | "boolean" | "datetime" | "select";
|
|
103
|
+
label: string;
|
|
104
|
+
required?: boolean;
|
|
105
|
+
options?: string[]; // For select sub-fields
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Allowed types for repeater sub-fields (no nesting, no complex types) */
|
|
109
|
+
export const REPEATER_SUB_FIELD_TYPES = [
|
|
110
|
+
"string",
|
|
111
|
+
"text",
|
|
112
|
+
"number",
|
|
113
|
+
"integer",
|
|
114
|
+
"boolean",
|
|
115
|
+
"datetime",
|
|
116
|
+
"select",
|
|
117
|
+
] as const;
|
|
118
|
+
|
|
96
119
|
export interface FieldValidation {
|
|
97
120
|
required?: boolean;
|
|
98
121
|
min?: number;
|
|
@@ -101,6 +124,9 @@ export interface FieldValidation {
|
|
|
101
124
|
maxLength?: number;
|
|
102
125
|
pattern?: string;
|
|
103
126
|
options?: string[]; // For select/multiSelect
|
|
127
|
+
subFields?: RepeaterSubField[]; // For repeater fields
|
|
128
|
+
minItems?: number; // For repeater fields
|
|
129
|
+
maxItems?: number; // For repeater fields
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
/**
|
|
@@ -360,9 +360,7 @@ export class FTSManager {
|
|
|
360
360
|
/**
|
|
361
361
|
* Verify FTS index integrity and rebuild if corrupted.
|
|
362
362
|
*
|
|
363
|
-
* Checks for
|
|
364
|
-
* 1. Row count mismatch between content table and FTS table
|
|
365
|
-
* 2. FTS5 integrity-check failure (catches shadow table inconsistencies)
|
|
363
|
+
* Checks for row count mismatch between content table and FTS table.
|
|
366
364
|
*
|
|
367
365
|
* Returns true if the index was rebuilt, false if it was healthy.
|
|
368
366
|
*/
|
|
@@ -401,21 +399,6 @@ export class FTSManager {
|
|
|
401
399
|
return true;
|
|
402
400
|
}
|
|
403
401
|
|
|
404
|
-
// Check 2: FTS5 integrity check (catches shadow table corruption)
|
|
405
|
-
try {
|
|
406
|
-
await sql
|
|
407
|
-
.raw(`INSERT INTO "${ftsTable}"("${ftsTable}") VALUES('integrity-check')`)
|
|
408
|
-
.execute(this.db);
|
|
409
|
-
} catch {
|
|
410
|
-
console.warn(`FTS integrity check failed for "${collectionSlug}". Rebuilding index.`);
|
|
411
|
-
const fields = await this.getSearchableFields(collectionSlug);
|
|
412
|
-
const config = await this.getSearchConfig(collectionSlug);
|
|
413
|
-
if (fields.length > 0) {
|
|
414
|
-
await this.rebuildIndex(collectionSlug, fields, config?.weights);
|
|
415
|
-
}
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
402
|
return false;
|
|
420
403
|
}
|
|
421
404
|
|
|
@@ -688,6 +688,11 @@ export function renderToolbar(config: ToolbarConfig): string {
|
|
|
688
688
|
return f ? f.kind : null;
|
|
689
689
|
}
|
|
690
690
|
|
|
691
|
+
// Load manifest early so the first click can resolve field kinds without racing the event.
|
|
692
|
+
if (isEditMode) {
|
|
693
|
+
fetchManifest();
|
|
694
|
+
}
|
|
695
|
+
|
|
691
696
|
// Save a single field value
|
|
692
697
|
function saveField(collection, id, field, value) {
|
|
693
698
|
setSaveState("saving");
|
|
@@ -1149,15 +1154,64 @@ export function renderToolbar(config: ToolbarConfig): string {
|
|
|
1149
1154
|
});
|
|
1150
1155
|
|
|
1151
1156
|
dimPromise.then(function(dims) {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1157
|
+
// Generate a thumbnail for large images to avoid OOM in server-side
|
|
1158
|
+
// blurhash generation on memory-constrained runtimes (Workers).
|
|
1159
|
+
// Thumbnail fits within a 64x64 box (scale by max dimension) so that
|
|
1160
|
+
// extreme aspect ratios don't explode into a huge canvas client-side.
|
|
1161
|
+
var thumbPromise;
|
|
1162
|
+
if (dims.width && dims.height && dims.width * dims.height * 4 > 32 * 1024 * 1024) {
|
|
1163
|
+
thumbPromise = new Promise(function(resolve) {
|
|
1164
|
+
try {
|
|
1165
|
+
var maxDim = Math.max(dims.width, dims.height);
|
|
1166
|
+
var scale = Math.min(1, 64 / maxDim);
|
|
1167
|
+
var thumbW = Math.max(1, Math.round(dims.width * scale));
|
|
1168
|
+
var thumbH = Math.max(1, Math.round(dims.height * scale));
|
|
1169
|
+
var canvas = document.createElement("canvas");
|
|
1170
|
+
canvas.width = thumbW;
|
|
1171
|
+
canvas.height = thumbH;
|
|
1172
|
+
var ctx = canvas.getContext("2d");
|
|
1173
|
+
if (ctx) {
|
|
1174
|
+
var img = new Image();
|
|
1175
|
+
img.onload = function() {
|
|
1176
|
+
try {
|
|
1177
|
+
ctx.drawImage(img, 0, 0, thumbW, thumbH);
|
|
1178
|
+
canvas.toBlob(function(blob) {
|
|
1179
|
+
URL.revokeObjectURL(img.src);
|
|
1180
|
+
resolve(blob);
|
|
1181
|
+
}, "image/png");
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
URL.revokeObjectURL(img.src);
|
|
1184
|
+
resolve(null);
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
img.onerror = function() {
|
|
1188
|
+
URL.revokeObjectURL(img.src);
|
|
1189
|
+
resolve(null);
|
|
1190
|
+
};
|
|
1191
|
+
img.src = URL.createObjectURL(file);
|
|
1192
|
+
} else {
|
|
1193
|
+
resolve(null);
|
|
1194
|
+
}
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
resolve(null);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
} else {
|
|
1200
|
+
thumbPromise = Promise.resolve(null);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return thumbPromise.then(function(thumbnail) {
|
|
1204
|
+
var formData = new FormData();
|
|
1205
|
+
formData.append("file", file);
|
|
1206
|
+
if (dims.width) formData.append("width", String(dims.width));
|
|
1207
|
+
if (dims.height) formData.append("height", String(dims.height));
|
|
1208
|
+
if (thumbnail) formData.append("thumbnail", thumbnail, "thumb.png");
|
|
1209
|
+
|
|
1210
|
+
return ecFetch("/_emdash/api/media", {
|
|
1211
|
+
method: "POST",
|
|
1212
|
+
credentials: "same-origin",
|
|
1213
|
+
body: formData
|
|
1214
|
+
});
|
|
1161
1215
|
});
|
|
1162
1216
|
})
|
|
1163
1217
|
.then(function(r) { return r.json(); })
|
|
@@ -1187,31 +1241,39 @@ export function renderToolbar(config: ToolbarConfig): string {
|
|
|
1187
1241
|
|
|
1188
1242
|
var ref = target.getAttribute && target.getAttribute("data-emdash-ref");
|
|
1189
1243
|
if (ref) {
|
|
1190
|
-
e.preventDefault();
|
|
1191
|
-
e.stopPropagation();
|
|
1192
|
-
|
|
1193
1244
|
try {
|
|
1194
1245
|
var annotation = JSON.parse(ref);
|
|
1195
1246
|
|
|
1196
|
-
// Entry-level annotation (no field) —
|
|
1197
|
-
if (!annotation.field)
|
|
1247
|
+
// Entry-level annotation (no field) — keep walking for a field-level ancestor
|
|
1248
|
+
if (!annotation.field) {
|
|
1249
|
+
target = target.parentElement;
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1198
1252
|
|
|
1199
|
-
|
|
1200
|
-
fetchManifest().then(function(manifest) {
|
|
1201
|
-
var kind = getFieldKind(manifest, annotation.collection, annotation.field);
|
|
1202
|
-
|
|
1203
|
-
// Close any open image popover before starting a new edit
|
|
1253
|
+
function dispatchInline(kind) {
|
|
1204
1254
|
closeImagePopover();
|
|
1205
|
-
|
|
1255
|
+
// Portable Text is edited in-page by InlinePortableTextEditor — do not open admin
|
|
1256
|
+
if (kind === "portableText") {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
e.preventDefault();
|
|
1260
|
+
e.stopPropagation();
|
|
1206
1261
|
if (kind === "string" || kind === "text") {
|
|
1207
1262
|
startTextEdit(target, annotation);
|
|
1208
1263
|
} else if (kind === "image") {
|
|
1209
1264
|
startImageEdit(target, annotation);
|
|
1210
1265
|
} else {
|
|
1211
|
-
// Fallback: open admin for unsupported types
|
|
1212
1266
|
openAdmin(annotation);
|
|
1213
1267
|
}
|
|
1214
|
-
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (manifestCache) {
|
|
1271
|
+
dispatchInline(getFieldKind(manifestCache, annotation.collection, annotation.field));
|
|
1272
|
+
} else {
|
|
1273
|
+
fetchManifest().then(function(manifest) {
|
|
1274
|
+
dispatchInline(getFieldKind(manifest, annotation.collection, annotation.field));
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1215
1277
|
} catch (err) {
|
|
1216
1278
|
console.error("Failed to parse emdash ref:", err);
|
|
1217
1279
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-schema-Dcl0R6nM.mjs","names":[],"sources":["../src/plugins/manifest-schema.ts"],"sourcesContent":["/**\n * Zod schema for PluginManifest validation\n *\n * Used to validate manifest.json from plugin bundles at every parse site:\n * - Client-side download (marketplace.ts extractBundle)\n * - R2 load (api/handlers/marketplace.ts loadBundleFromR2)\n * - CLI publish preview (cli/commands/publish.ts readManifestFromTarball)\n * - Marketplace ingest extends this with publishing-specific fields\n */\n\nimport { z } from \"zod\";\n\n// ── Enum values (must stay in sync with types.ts) ───────────────\n\nexport const PLUGIN_CAPABILITIES = [\n\t\"network:fetch\",\n\t\"network:fetch:any\",\n\t\"read:content\",\n\t\"write:content\",\n\t\"read:media\",\n\t\"write:media\",\n\t\"read:users\",\n\t\"email:send\",\n\t\"email:provide\",\n\t\"email:intercept\",\n\t\"page:inject\",\n] as const;\n\n/** Must stay in sync with FieldType in schema/types.ts */\nconst FIELD_TYPES = [\n\t\"string\",\n\t\"text\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n] as const;\n\nexport const HOOK_NAMES = [\n\t\"plugin:install\",\n\t\"plugin:activate\",\n\t\"plugin:deactivate\",\n\t\"plugin:uninstall\",\n\t\"content:beforeSave\",\n\t\"content:afterSave\",\n\t\"content:beforeDelete\",\n\t\"content:afterDelete\",\n\t\"media:beforeUpload\",\n\t\"media:afterUpload\",\n\t\"cron\",\n\t\"email:beforeSend\",\n\t\"email:deliver\",\n\t\"email:afterSend\",\n\t\"comment:beforeCreate\",\n\t\"comment:moderate\",\n\t\"comment:afterCreate\",\n\t\"comment:afterModerate\",\n\t\"page:metadata\",\n\t\"page:fragments\",\n] as const;\n\n/**\n * Structured hook entry for manifest — name plus optional metadata.\n * During a transition period, both plain strings and objects are accepted.\n */\nconst manifestHookEntrySchema = z.object({\n\tname: z.enum(HOOK_NAMES),\n\texclusive: z.boolean().optional(),\n\tpriority: z.number().int().optional(),\n\ttimeout: z.number().int().positive().optional(),\n});\n\n/**\n * Structured route entry for manifest — name plus optional metadata.\n * Both plain strings and objects are accepted; strings are normalized\n * to `{ name }` objects via `normalizeManifestRoute()`.\n */\n/** Route names must be safe path segments — alphanumeric, hyphens, underscores, forward slashes */\nconst routeNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9_\\-/]*$/;\n\nconst manifestRouteEntrySchema = z.object({\n\tname: z.string().min(1).regex(routeNamePattern, \"Route name must be a safe path segment\"),\n\tpublic: z.boolean().optional(),\n});\n\n// ── Sub-schemas ─────────────────────────────────────────────────\n\n/** Index field names must be valid identifiers to prevent SQL injection via JSON path expressions */\nconst indexFieldName = z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/);\n\nconst storageCollectionSchema = z.object({\n\tindexes: z.array(z.union([indexFieldName, z.array(indexFieldName)])),\n\tuniqueIndexes: z.array(z.union([indexFieldName, z.array(indexFieldName)])).optional(),\n});\n\nconst baseSettingFields = {\n\tlabel: z.string(),\n\tdescription: z.string().optional(),\n};\n\nconst settingFieldSchema = z.discriminatedUnion(\"type\", [\n\tz.object({\n\t\t...baseSettingFields,\n\t\ttype: z.literal(\"string\"),\n\t\tdefault: z.string().optional(),\n\t\tmultiline: z.boolean().optional(),\n\t}),\n\tz.object({\n\t\t...baseSettingFields,\n\t\ttype: z.literal(\"number\"),\n\t\tdefault: z.number().optional(),\n\t\tmin: z.number().optional(),\n\t\tmax: z.number().optional(),\n\t}),\n\tz.object({ ...baseSettingFields, type: z.literal(\"boolean\"), default: z.boolean().optional() }),\n\tz.object({\n\t\t...baseSettingFields,\n\t\ttype: z.literal(\"select\"),\n\t\toptions: z.array(z.object({ value: z.string(), label: z.string() })),\n\t\tdefault: z.string().optional(),\n\t}),\n\tz.object({ ...baseSettingFields, type: z.literal(\"secret\") }),\n]);\n\nconst adminPageSchema = z.object({\n\tpath: z.string(),\n\tlabel: z.string(),\n\ticon: z.string().optional(),\n});\n\nconst dashboardWidgetSchema = z.object({\n\tid: z.string(),\n\tsize: z.enum([\"full\", \"half\", \"third\"]).optional(),\n\ttitle: z.string().optional(),\n});\n\nconst pluginAdminConfigSchema = z.object({\n\tentry: z.string().optional(),\n\tsettingsSchema: z.record(z.string(), settingFieldSchema).optional(),\n\tpages: z.array(adminPageSchema).optional(),\n\twidgets: z.array(dashboardWidgetSchema).optional(),\n\tfieldWidgets: z\n\t\t.array(\n\t\t\tz.object({\n\t\t\t\tname: z.string().min(1),\n\t\t\t\tlabel: z.string().min(1),\n\t\t\t\tfieldTypes: z.array(z.enum(FIELD_TYPES)),\n\t\t\t\telements: z\n\t\t\t\t\t.array(\n\t\t\t\t\t\tz\n\t\t\t\t\t\t\t.object({\n\t\t\t\t\t\t\t\ttype: z.string(),\n\t\t\t\t\t\t\t\taction_id: z.string(),\n\t\t\t\t\t\t\t\tlabel: z.string().optional(),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.passthrough(),\n\t\t\t\t\t)\n\t\t\t\t\t.optional(),\n\t\t\t}),\n\t\t)\n\t\t.optional(),\n});\n\n// ── Main schema ─────────────────────────────────────────────────\n\n/**\n * Zod schema matching the PluginManifest interface from types.ts.\n *\n * Every JSON.parse of a manifest.json should validate through this.\n */\nexport const pluginManifestSchema = z.object({\n\tid: z.string().min(1),\n\tversion: z.string().min(1),\n\tcapabilities: z.array(z.enum(PLUGIN_CAPABILITIES)),\n\tallowedHosts: z.array(z.string()),\n\tstorage: z.record(z.string(), storageCollectionSchema),\n\t/**\n\t * Hook declarations — accepts both plain name strings (legacy) and\n\t * structured objects with exclusive/priority/timeout metadata.\n\t * Plain strings are normalized to `{ name }` objects after parsing.\n\t */\n\thooks: z.array(z.union([z.enum(HOOK_NAMES), manifestHookEntrySchema])),\n\t/**\n\t * Route declarations — accepts both plain name strings and\n\t * structured objects with public metadata.\n\t * Plain strings are normalized to `{ name }` objects after parsing.\n\t */\n\troutes: z.array(\n\t\tz.union([\n\t\t\tz.string().min(1).regex(routeNamePattern, \"Route name must be a safe path segment\"),\n\t\t\tmanifestRouteEntrySchema,\n\t\t]),\n\t),\n\tadmin: pluginAdminConfigSchema,\n});\n\nexport type ValidatedPluginManifest = z.infer<typeof pluginManifestSchema>;\n\n/**\n * Normalize a manifest hook entry — plain strings become `{ name }` objects.\n */\nexport function normalizeManifestHook(\n\tentry: string | { name: string; exclusive?: boolean; priority?: number; timeout?: number },\n): { name: string; exclusive?: boolean; priority?: number; timeout?: number } {\n\tif (typeof entry === \"string\") {\n\t\treturn { name: entry };\n\t}\n\treturn entry;\n}\n\n/**\n * Normalize a manifest route entry — plain strings become `{ name }` objects.\n */\nexport function normalizeManifestRoute(entry: string | { name: string; public?: boolean }): {\n\tname: string;\n\tpublic?: boolean;\n} {\n\tif (typeof entry === \"string\") {\n\t\treturn { name: entry };\n\t}\n\treturn entry;\n}\n"],"mappings":"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"}
|