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.
Files changed (111) hide show
  1. package/LICENSE +9 -0
  2. package/dist/{apply-Bjfq_b4-.mjs → apply-kC39ev1Z.mjs} +4 -4
  3. package/dist/{apply-Bjfq_b4-.mjs.map → apply-kC39ev1Z.mjs.map} +1 -1
  4. package/dist/astro/index.d.mts +3 -3
  5. package/dist/astro/index.mjs +16 -1
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/middleware/auth.d.mts +3 -3
  8. package/dist/astro/middleware/request-context.mjs +84 -22
  9. package/dist/astro/middleware/request-context.mjs.map +1 -1
  10. package/dist/astro/middleware.mjs +41 -12
  11. package/dist/astro/middleware.mjs.map +1 -1
  12. package/dist/astro/types.d.mts +5 -4
  13. package/dist/astro/types.d.mts.map +1 -1
  14. package/dist/cli/index.mjs +65 -6
  15. package/dist/cli/index.mjs.map +1 -1
  16. package/dist/db/index.mjs +1 -1
  17. package/dist/{index-C1xF3OGh.d.mts → index-CLBc4gw-.d.mts} +42 -11
  18. package/dist/{index-C1xF3OGh.d.mts.map → index-CLBc4gw-.d.mts.map} +1 -1
  19. package/dist/index.d.mts +5 -5
  20. package/dist/index.mjs +9 -9
  21. package/dist/{manifest-schema-Dcl0R6nM.mjs → manifest-schema-CL8DWO9b.mjs} +5 -2
  22. package/dist/manifest-schema-CL8DWO9b.mjs.map +1 -0
  23. package/dist/media/index.d.mts +1 -1
  24. package/dist/media/index.mjs +1 -1
  25. package/dist/media/local-runtime.d.mts +4 -4
  26. package/dist/page/index.d.mts +1 -1
  27. package/dist/{placeholder-CmGAmqeO.d.mts → placeholder-SvFCKbz_.d.mts} +10 -2
  28. package/dist/{placeholder-CmGAmqeO.d.mts.map → placeholder-SvFCKbz_.d.mts.map} +1 -1
  29. package/dist/{placeholder-SmpOx-_v.mjs → placeholder-aiCD8aSZ.mjs} +27 -2
  30. package/dist/placeholder-aiCD8aSZ.mjs.map +1 -0
  31. package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
  32. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  33. package/dist/{query-CS_iSj34.mjs → query-BVYN0PJ6.mjs} +2 -2
  34. package/dist/{query-CS_iSj34.mjs.map → query-BVYN0PJ6.mjs.map} +1 -1
  35. package/dist/{registry-D_w5HW4G.mjs → registry-BNYQKX_d.mjs} +23 -38
  36. package/dist/registry-BNYQKX_d.mjs.map +1 -0
  37. package/dist/{runner-C0hCbYnD.mjs → runner-BraqvGYk.mjs} +251 -158
  38. package/dist/runner-BraqvGYk.mjs.map +1 -0
  39. package/dist/runner-EAtf0ZIe.d.mts.map +1 -1
  40. package/dist/runtime.d.mts +4 -4
  41. package/dist/{search-DG603UrT.mjs → search-C1gg67nN.mjs} +125 -18
  42. package/dist/search-C1gg67nN.mjs.map +1 -0
  43. package/dist/seed/index.d.mts +1 -1
  44. package/dist/seed/index.mjs +3 -3
  45. package/dist/{types-DvhsUmSJ.d.mts → types-BQo5JS0J.d.mts} +15 -2
  46. package/dist/{types-DvhsUmSJ.d.mts.map → types-BQo5JS0J.d.mts.map} +1 -1
  47. package/dist/{types-DY5zk5HN.mjs → types-CiA5Gac0.mjs} +5 -3
  48. package/dist/types-CiA5Gac0.mjs.map +1 -0
  49. package/dist/{types-C4-fAxN3.d.mts → types-DPfzHnjW.d.mts} +13 -2
  50. package/dist/types-DPfzHnjW.d.mts.map +1 -0
  51. package/dist/{validate-CpBtVMsD.d.mts → validate-HtxZeaBi.d.mts} +2 -2
  52. package/dist/{validate-CpBtVMsD.d.mts.map → validate-HtxZeaBi.d.mts.map} +1 -1
  53. package/dist/{validate-O7PWmlnq.mjs → validate-_rsF-Dx_.mjs} +2 -2
  54. package/dist/{validate-O7PWmlnq.mjs.map → validate-_rsF-Dx_.mjs.map} +1 -1
  55. package/package.json +6 -4
  56. package/src/api/handlers/marketplace.ts +7 -4
  57. package/src/api/schemas/schema.ts +12 -0
  58. package/src/astro/integration/index.ts +17 -0
  59. package/src/astro/integration/runtime.ts +13 -0
  60. package/src/astro/integration/virtual-modules.ts +13 -1
  61. package/src/astro/routes/admin.astro +1 -1
  62. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +3 -1
  63. package/src/astro/routes/api/auth/invite/complete.ts +2 -1
  64. package/src/astro/routes/api/auth/passkey/options.ts +2 -1
  65. package/src/astro/routes/api/auth/passkey/register/options.ts +2 -1
  66. package/src/astro/routes/api/auth/passkey/register/verify.ts +2 -1
  67. package/src/astro/routes/api/auth/passkey/verify.ts +2 -1
  68. package/src/astro/routes/api/auth/signup/complete.ts +2 -1
  69. package/src/astro/routes/api/import/wordpress/analyze.ts +24 -3
  70. package/src/astro/routes/api/import/wordpress/execute.ts +5 -1
  71. package/src/astro/routes/api/import/wordpress/prepare.ts +2 -2
  72. package/src/astro/routes/api/media.ts +16 -4
  73. package/src/astro/routes/api/search/index.ts +1 -5
  74. package/src/astro/routes/api/search/suggest.ts +1 -5
  75. package/src/astro/routes/api/setup/admin-verify.ts +2 -1
  76. package/src/astro/routes/api/setup/admin.ts +2 -1
  77. package/src/astro/types.ts +1 -0
  78. package/src/auth/passkey-config.ts +24 -3
  79. package/src/cli/commands/bundle-utils.ts +26 -0
  80. package/src/cli/commands/bundle.ts +15 -0
  81. package/src/cli/commands/content.ts +11 -1
  82. package/src/cli/commands/login.ts +2 -0
  83. package/src/cli/commands/media.ts +5 -1
  84. package/src/cli/commands/menu.ts +3 -1
  85. package/src/cli/commands/schema.ts +7 -1
  86. package/src/cli/commands/search-cmd.ts +2 -1
  87. package/src/cli/commands/taxonomy.ts +4 -1
  88. package/src/cli/output.ts +14 -0
  89. package/src/components/InlinePortableTextEditor.tsx +33 -3
  90. package/src/database/migrations/033_optimize_content_indexes.ts +113 -0
  91. package/src/database/migrations/runner.ts +40 -33
  92. package/src/database/repositories/comment.ts +32 -20
  93. package/src/emdash-runtime.ts +64 -2
  94. package/src/media/placeholder.ts +31 -0
  95. package/src/media/thumbnail.ts +32 -0
  96. package/src/plugins/hooks.ts +91 -0
  97. package/src/plugins/manager.ts +22 -0
  98. package/src/plugins/manifest-schema.ts +3 -0
  99. package/src/plugins/marketplace.ts +25 -12
  100. package/src/plugins/types.ts +24 -0
  101. package/src/schema/registry.ts +23 -27
  102. package/src/schema/types.ts +27 -1
  103. package/src/search/fts-manager.ts +1 -18
  104. package/src/visual-editing/toolbar.ts +84 -22
  105. package/dist/manifest-schema-Dcl0R6nM.mjs.map +0 -1
  106. package/dist/placeholder-SmpOx-_v.mjs.map +0 -1
  107. package/dist/registry-D_w5HW4G.mjs.map +0 -1
  108. package/dist/runner-C0hCbYnD.mjs.map +0 -1
  109. package/dist/search-DG603UrT.mjs.map +0 -1
  110. package/dist/types-C4-fAxN3.d.mts.map +0 -1
  111. package/dist/types-DY5zk5HN.mjs.map +0 -1
@@ -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
  // =========================================================================
@@ -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
- /** Generate a stable non-identifying site hash (best-effort) */
437
- async function generateSiteHash(): Promise<string> {
438
- // Use a timestamp-based approach since we can't reliably get the origin
439
- // in all contexts (Workers, Node, etc.)
440
- const seed = `emdash-${Date.now()}`;
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
- return Math.random().toString(36).slice(2, 18);
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
  }
@@ -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>;
@@ -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}_status`)}
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}_created`)}
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}_updated`)}
590
- ON ${sql.ref(tableName)} (updated_at)
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}_locale`)}
595
- ON ${sql.ref(tableName)} (locale)
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}_translation_group`)}
600
- ON ${sql.ref(tableName)} (translation_group)
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
 
@@ -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 two corruption indicators:
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
- var formData = new FormData();
1153
- formData.append("file", file);
1154
- if (dims.width) formData.append("width", String(dims.width));
1155
- if (dims.height) formData.append("height", String(dims.height));
1156
-
1157
- return ecFetch("/_emdash/api/media", {
1158
- method: "POST",
1159
- credentials: "same-origin",
1160
- body: formData
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) — ignore, it's a container
1197
- if (!annotation.field) return;
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
- // Fetch manifest to determine field type, then dispatch
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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"}