@jini-ai/cms 0.5.2 → 0.5.3

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 (205) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/media/index.d.ts +0 -3
  46. package/dist/media/index.js +1 -4
  47. package/dist/media/node.d.ts +5 -0
  48. package/dist/media/node.js +6 -0
  49. package/dist/redirects/capture.d.ts +80 -0
  50. package/dist/redirects/capture.js +66 -0
  51. package/dist/redirects/express/index.d.ts +27 -0
  52. package/dist/redirects/express/index.js +40 -0
  53. package/dist/redirects/hit-sink.d.ts +56 -0
  54. package/dist/redirects/hit-sink.js +44 -0
  55. package/dist/redirects/host-config/index.d.ts +40 -0
  56. package/dist/redirects/host-config/index.js +145 -0
  57. package/dist/redirects/index.d.ts +14 -0
  58. package/dist/redirects/index.js +14 -0
  59. package/dist/redirects/matcher.d.ts +58 -0
  60. package/dist/redirects/matcher.js +144 -0
  61. package/dist/redirects/order.d.ts +30 -0
  62. package/dist/redirects/order.js +26 -0
  63. package/dist/redirects/ports.d.ts +242 -0
  64. package/dist/redirects/ports.internal.d.ts +57 -0
  65. package/dist/redirects/ports.internal.js +14 -0
  66. package/dist/redirects/ports.js +2 -0
  67. package/dist/redirects/redirects.d.ts +226 -0
  68. package/dist/redirects/redirects.js +590 -0
  69. package/dist/redirects/referrer-alias.d.ts +31 -0
  70. package/dist/redirects/referrer-alias.js +33 -0
  71. package/dist/redirects/repo.memory.d.ts +51 -0
  72. package/dist/redirects/repo.memory.js +98 -0
  73. package/dist/redirects/reserved-destination.d.ts +58 -0
  74. package/dist/redirects/reserved-destination.js +59 -0
  75. package/dist/redirects/resolver.d.ts +100 -0
  76. package/dist/redirects/resolver.js +87 -0
  77. package/dist/redirects/spa/index.d.ts +30 -0
  78. package/dist/redirects/spa/index.js +27 -0
  79. package/dist/redirects/sql/index.d.ts +4 -0
  80. package/dist/redirects/sql/index.js +4 -0
  81. package/dist/redirects/sql/repo.d.ts +82 -0
  82. package/dist/redirects/sql/repo.js +143 -0
  83. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  84. package/dist/redirects/sql/repo.rows.js +79 -0
  85. package/dist/redirects/sql/row-types.d.ts +34 -0
  86. package/dist/redirects/sql/row-types.js +2 -0
  87. package/dist/redirects/target-gate.d.ts +30 -0
  88. package/dist/redirects/target-gate.js +85 -0
  89. package/dist/redirects/types.d.ts +213 -0
  90. package/dist/redirects/types.js +42 -0
  91. package/dist/seo/absolute-url.d.ts +58 -0
  92. package/dist/seo/absolute-url.js +117 -0
  93. package/dist/seo/dom/index.d.ts +14 -0
  94. package/dist/seo/dom/index.js +52 -0
  95. package/dist/seo/errors.d.ts +35 -0
  96. package/dist/seo/errors.js +35 -0
  97. package/dist/seo/feed.d.ts +49 -0
  98. package/dist/seo/feed.js +61 -0
  99. package/dist/seo/index.d.ts +13 -0
  100. package/dist/seo/index.js +13 -0
  101. package/dist/seo/media.d.ts +57 -0
  102. package/dist/seo/media.js +134 -0
  103. package/dist/seo/page-head.d.ts +18 -0
  104. package/dist/seo/page-head.js +114 -0
  105. package/dist/seo/ports.d.ts +221 -0
  106. package/dist/seo/ports.js +27 -0
  107. package/dist/seo/seo.d.ts +73 -0
  108. package/dist/seo/seo.js +230 -0
  109. package/dist/seo/sitemap.d.ts +139 -0
  110. package/dist/seo/sitemap.js +217 -0
  111. package/dist/seo/types.d.ts +278 -0
  112. package/dist/seo/types.js +2 -0
  113. package/dist/seo/write-service.d.ts +79 -0
  114. package/dist/seo/write-service.js +273 -0
  115. package/dist/seo/xml-escape.d.ts +16 -0
  116. package/dist/seo/xml-escape.js +19 -0
  117. package/dist/settings/express/cms-adapter.d.ts +20 -0
  118. package/dist/settings/express/cms-adapter.js +124 -0
  119. package/dist/settings/express/contracts.d.ts +135 -0
  120. package/dist/settings/express/contracts.js +2 -0
  121. package/dist/settings/express/http.d.ts +10 -0
  122. package/dist/settings/express/http.js +211 -0
  123. package/dist/settings/express/index.d.ts +5 -0
  124. package/dist/settings/express/index.js +5 -0
  125. package/dist/settings/express/shared.d.ts +68 -0
  126. package/dist/settings/express/shared.js +54 -0
  127. package/dist/settings/express/stream.d.ts +24 -0
  128. package/dist/settings/express/stream.js +147 -0
  129. package/dist/settings/index.d.ts +3 -38
  130. package/dist/settings/index.js +3 -35
  131. package/dist/taxonomy/write-service.d.ts +2 -30
  132. package/dist/widgets/authorize-helper.d.ts +42 -0
  133. package/dist/widgets/authorize-helper.js +16 -0
  134. package/dist/widgets/concurrency.d.ts +12 -0
  135. package/dist/widgets/concurrency.js +44 -0
  136. package/dist/widgets/config-validation.d.ts +34 -0
  137. package/dist/widgets/config-validation.js +125 -0
  138. package/dist/widgets/embed-service.d.ts +133 -0
  139. package/dist/widgets/embed-service.js +460 -0
  140. package/dist/widgets/embed-validation.d.ts +23 -0
  141. package/dist/widgets/embed-validation.js +38 -0
  142. package/dist/widgets/entry-payload.d.ts +103 -0
  143. package/dist/widgets/entry-payload.js +160 -0
  144. package/dist/widgets/errors.d.ts +136 -0
  145. package/dist/widgets/errors.js +136 -0
  146. package/dist/widgets/host-ports.d.ts +149 -0
  147. package/dist/widgets/host-ports.js +2 -0
  148. package/dist/widgets/html/html-embeds.d.ts +247 -0
  149. package/dist/widgets/html/html-embeds.js +408 -0
  150. package/dist/widgets/html/index.d.ts +5 -0
  151. package/dist/widgets/html/index.js +4 -0
  152. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  153. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  154. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  155. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  156. package/dist/widgets/index.d.ts +20 -0
  157. package/dist/widgets/index.js +19 -0
  158. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  159. package/dist/widgets/markers/html-attributes.js +216 -0
  160. package/dist/widgets/markers/index.d.ts +3 -0
  161. package/dist/widgets/markers/index.js +3 -0
  162. package/dist/widgets/markers/marker.d.ts +496 -0
  163. package/dist/widgets/markers/marker.js +780 -0
  164. package/dist/widgets/page-embed-types.d.ts +42 -0
  165. package/dist/widgets/page-embed-types.js +42 -0
  166. package/dist/widgets/ports.d.ts +142 -0
  167. package/dist/widgets/ports.js +2 -0
  168. package/dist/widgets/read-service.d.ts +81 -0
  169. package/dist/widgets/read-service.js +79 -0
  170. package/dist/widgets/region-area-service.d.ts +110 -0
  171. package/dist/widgets/region-area-service.js +180 -0
  172. package/dist/widgets/registry.d.ts +217 -0
  173. package/dist/widgets/registry.js +172 -0
  174. package/dist/widgets/repo.memory.d.ts +41 -0
  175. package/dist/widgets/repo.memory.js +48 -0
  176. package/dist/widgets/resolver-service.d.ts +209 -0
  177. package/dist/widgets/resolver-service.js +909 -0
  178. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  179. package/dist/widgets/resolvers/contact-form.js +54 -0
  180. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  181. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  182. package/dist/widgets/resolvers/index.d.ts +65 -0
  183. package/dist/widgets/resolvers/index.js +155 -0
  184. package/dist/widgets/resolvers/menu.d.ts +42 -0
  185. package/dist/widgets/resolvers/menu.js +43 -0
  186. package/dist/widgets/resolvers/ports.d.ts +82 -0
  187. package/dist/widgets/resolvers/ports.js +2 -0
  188. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  189. package/dist/widgets/resolvers/recent-entries.js +165 -0
  190. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  191. package/dist/widgets/resolvers/taxonomy.js +34 -0
  192. package/dist/widgets/sql/index.d.ts +3 -0
  193. package/dist/widgets/sql/index.js +3 -0
  194. package/dist/widgets/sql/repo.d.ts +41 -0
  195. package/dist/widgets/sql/repo.js +82 -0
  196. package/dist/widgets/sql/tables.d.ts +13 -0
  197. package/dist/widgets/sql/tables.js +2 -0
  198. package/dist/widgets/types.d.ts +213 -0
  199. package/dist/widgets/types.js +17 -0
  200. package/dist/widgets/where-used.d.ts +41 -0
  201. package/dist/widgets/where-used.js +11 -0
  202. package/dist/widgets/write-service.d.ts +204 -0
  203. package/dist/widgets/write-service.js +332 -0
  204. package/dist/workspace/delete.d.ts +1 -1
  205. package/package.json +220 -11
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file The page-embed stage's own marker types, as a dependency-free leaf.
3
+ *
4
+ * `resolver-service.ts`'s `HTML_EMBED_RESOLVERS` is the registry these name; it `satisfies`
5
+ * `Record<PageEmbedType, …>`, so the compiler fails if the two ever disagree in either direction.
6
+ * The list lives here rather than being read off that registry because `render.ts` needs
7
+ * `isPageEmbedType`, and `render.ts` is loaded by every theme render worker (`handlebars-worker.ts`,
8
+ * `liquid-worker.ts`). Importing `resolver-service.ts` from there pulled its post/media/entries
9
+ * services, and through them the DB kernels and the S3 blob client, into every worker spawn:
10
+ * ~990 modules and seconds of cold start per render (see `__tests__/worker-import-graph.test.ts`
11
+ * under `server/inbound/public-http/http/site/`).
12
+ */
13
+ export declare const PAGE_EMBED_TYPES: readonly ["widget", "form", "taxonomy", "media", "post", "content"];
14
+ export type PageEmbedType = (typeof PAGE_EMBED_TYPES)[number];
15
+ /**
16
+ * Does the page-embed stage OWN this marker type — i.e. is a REQ-28 placeholder the honest answer
17
+ * when it fails to resolve?
18
+ *
19
+ * This question did not exist before the 2026-08-10 marker unification, and its absence was a real
20
+ * bug for exactly as long as the unification was half-done. `html-embeds.ts` used to match only an
21
+ * empty `<div data-embed-type="…">`, so a theme's own `partial`/`menu` markers were INVISIBLE to
22
+ * this stage — "unknown type" could only ever mean an author's typo, and rendering the REQ-28
23
+ * placeholder for it was right. Sharing one permissive parser made every marker visible to every
24
+ * consumer, so `renderHtmlPageBody` began substituting placeholders over the nav, the docs menu, and
25
+ * the footer of any post rendered through a theme template — three markers a LATER stage
26
+ * (`static-render.ts`'s `resolveSlots`/`injectMenuEmbeds`) owns and would have resolved.
27
+ *
28
+ * So ownership must be asked of the resolver registry (`resolver-service.ts`'s
29
+ * `HTML_EMBED_RESOLVERS`, whose keys are exactly {@link PAGE_EMBED_TYPES}), never inferred from
30
+ * "did resolution produce anything". A type present there that failed to resolve still degrades to
31
+ * the placeholder — that is REQ-28 and unchanged. A type absent from there is not this stage's to
32
+ * render OR to blank: it is left exactly as authored, which is the shared parser's own "unresolved
33
+ * means untouched" invariant.
34
+ *
35
+ * The cost of being wrong is asymmetric and that is why the default is untouched: a marker wrongly
36
+ * left alone is visible in the output the moment anyone looks at the page, while a marker wrongly
37
+ * replaced is a silently-deleted nav that renders as a tidy, plausible page with a hole in it.
38
+ */
39
+ export declare function isPageEmbedType({ type }: {
40
+ type: string;
41
+ }, _optional?: Record<string, never>): boolean;
42
+ //# sourceMappingURL=page-embed-types.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file The page-embed stage's own marker types, as a dependency-free leaf.
3
+ *
4
+ * `resolver-service.ts`'s `HTML_EMBED_RESOLVERS` is the registry these name; it `satisfies`
5
+ * `Record<PageEmbedType, …>`, so the compiler fails if the two ever disagree in either direction.
6
+ * The list lives here rather than being read off that registry because `render.ts` needs
7
+ * `isPageEmbedType`, and `render.ts` is loaded by every theme render worker (`handlebars-worker.ts`,
8
+ * `liquid-worker.ts`). Importing `resolver-service.ts` from there pulled its post/media/entries
9
+ * services, and through them the DB kernels and the S3 blob client, into every worker spawn:
10
+ * ~990 modules and seconds of cold start per render (see `__tests__/worker-import-graph.test.ts`
11
+ * under `server/inbound/public-http/http/site/`).
12
+ */
13
+ export const PAGE_EMBED_TYPES = ["widget", "form", "taxonomy", "media", "post", "content"];
14
+ const PAGE_EMBED_TYPE_SET = new Set(PAGE_EMBED_TYPES);
15
+ /**
16
+ * Does the page-embed stage OWN this marker type — i.e. is a REQ-28 placeholder the honest answer
17
+ * when it fails to resolve?
18
+ *
19
+ * This question did not exist before the 2026-08-10 marker unification, and its absence was a real
20
+ * bug for exactly as long as the unification was half-done. `html-embeds.ts` used to match only an
21
+ * empty `<div data-embed-type="…">`, so a theme's own `partial`/`menu` markers were INVISIBLE to
22
+ * this stage — "unknown type" could only ever mean an author's typo, and rendering the REQ-28
23
+ * placeholder for it was right. Sharing one permissive parser made every marker visible to every
24
+ * consumer, so `renderHtmlPageBody` began substituting placeholders over the nav, the docs menu, and
25
+ * the footer of any post rendered through a theme template — three markers a LATER stage
26
+ * (`static-render.ts`'s `resolveSlots`/`injectMenuEmbeds`) owns and would have resolved.
27
+ *
28
+ * So ownership must be asked of the resolver registry (`resolver-service.ts`'s
29
+ * `HTML_EMBED_RESOLVERS`, whose keys are exactly {@link PAGE_EMBED_TYPES}), never inferred from
30
+ * "did resolution produce anything". A type present there that failed to resolve still degrades to
31
+ * the placeholder — that is REQ-28 and unchanged. A type absent from there is not this stage's to
32
+ * render OR to blank: it is left exactly as authored, which is the shared parser's own "unresolved
33
+ * means untouched" invariant.
34
+ *
35
+ * The cost of being wrong is asymmetric and that is why the default is untouched: a marker wrongly
36
+ * left alone is visible in the output the moment anyone looks at the page, while a marker wrongly
37
+ * replaced is a silently-deleted nav that renders as a tidy, plausible page with a hole in it.
38
+ */
39
+ export function isPageEmbedType({ type }, _optional = {}) {
40
+ return PAGE_EMBED_TYPE_SET.has(type);
41
+ }
42
+ //# sourceMappingURL=page-embed-types.js.map
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @file Ports + typed-call contracts for the Tovu `widgets` library (ADR-047, SPEC-043).
3
+ *
4
+ * ADR-006 (rule-of-two) applied the way ADR-029 §5 already set precedent:
5
+ *
6
+ * - **`WidgetRegionBindingRepoPort` IS a port.** It persists the derived
7
+ * `widget_region_bindings` index — two real adapters, in-memory + SQLite,
8
+ * mirroring `NavLocationBindingRepoPort` exactly.
9
+ *
10
+ * - **Widget instances add NO new persistence port.** A widget instance is an
11
+ * ADR-022 entry; it rides the existing entries repo (already rule-of-two).
12
+ *
13
+ * - **`widget_area` entries add NO new persistence port either** — same
14
+ * reasoning, they are entries too (ADR-047 Debate Fold-In Amendment 1).
15
+ *
16
+ * - **Resolution (`WidgetResolver`, `widgets/types.ts`) is NOT a port** — one
17
+ * evaluator per registered type, dispatched through the closed
18
+ * `CORE_RESOLVERS` map (`resolvers/index.ts`), not injected. Promote to a
19
+ * port only if a second real implementation of a given type's resolution
20
+ * logic appears (ADR-029 §5's anti-port-mania call, applied identically).
21
+ *
22
+ * INTERFACES ONLY. No feature logic lives here.
23
+ */
24
+ import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
25
+ import type { WidgetRegionBindingRow, WidgetRegionDescriptor, WidgetRegionKey } from "./types.js";
26
+ /**
27
+ * Persistence for the derived `widget_region_bindings` index. Writes happen
28
+ * only inside the same transaction as the `widget_area` entry mutation that
29
+ * changed `regionKey` (single write chokepoint, ADR-022 §4a) — this port is
30
+ * the storage seam, not a second write path. `rebuildForWorkspace` exists
31
+ * because the index is **rebuildable by definition** — a full rescan of
32
+ * `widget_area` entries can always reconstruct it (INV-02).
33
+ */
34
+ export interface WidgetRegionBindingRepoPort {
35
+ /** The `widget_area` entry currently bound to a region, if any. */
36
+ findByRegion(required: {
37
+ workspaceId: UUID;
38
+ regionKey: WidgetRegionKey;
39
+ }): Promise<WidgetRegionBindingRow | null>;
40
+ /** Every binding in the workspace (admin overview + render prefetch). */
41
+ listByWorkspace(required: {
42
+ workspaceId: UUID;
43
+ }): Promise<WidgetRegionBindingRow[]>;
44
+ /**
45
+ * Assign a region to a `widget_area` entry. Honors
46
+ * `UNIQUE (workspace_id, region_key)`. Called only from
47
+ * `region-area-service.ts`'s `reconcileWidgetRegionBindings` — never
48
+ * directly by a route handler or any other caller (INV-02).
49
+ */
50
+ upsert(required: {
51
+ workspaceId: UUID;
52
+ regionKey: WidgetRegionKey;
53
+ areaEntryId: UUID;
54
+ updatedAt: ISODateTime;
55
+ }): Promise<WidgetRegionBindingRow>;
56
+ /** Mark a region binding inactive (theme switch orphaned the region key — REQ-14). Never deletes the underlying `widget_area` entry. */
57
+ markInactive(required: {
58
+ workspaceId: UUID;
59
+ regionKey: WidgetRegionKey;
60
+ }): Promise<void>;
61
+ /**
62
+ * Rebuild the whole workspace's index from the authoritative `widget_area`
63
+ * entries. The index is derived + rebuildable, so this is always safe.
64
+ */
65
+ rebuildForWorkspace(required: {
66
+ workspaceId: UUID;
67
+ bindings: readonly WidgetRegionBindingRow[];
68
+ }): Promise<void>;
69
+ }
70
+ /** Registry of theme/plugin-declared widget regions (mirrors `NavLocationRegistry`). */
71
+ export interface WidgetRegionRegistry {
72
+ list(required: {
73
+ workspaceId: UUID;
74
+ }): WidgetRegionDescriptor[];
75
+ get(required: {
76
+ workspaceId: UUID;
77
+ key: WidgetRegionKey;
78
+ }): WidgetRegionDescriptor | null;
79
+ }
80
+ /**
81
+ * The function `trashWidgetInstance` receives to move a widget to the Trash, bound to the `"widget"`
82
+ * type at the composition root. Structurally typed on purpose: this feature imports nothing from
83
+ * `features/trash`.
84
+ */
85
+ export type RemoveWidgetFn = (required: {
86
+ workspaceId: UUID;
87
+ id: UUID;
88
+ display: {
89
+ title: string;
90
+ subtitle?: string | null;
91
+ };
92
+ at: string;
93
+ expectedVersion: number | null;
94
+ actor: {
95
+ principalId: string;
96
+ pluginId?: string | null;
97
+ };
98
+ /** The status a restore brings back, when the Trash cannot read it off a column — only the
99
+ * legacy-widget adoption states one (`"active"`). */
100
+ priorMarker?: string | null;
101
+ }) => Promise<{
102
+ ok: true;
103
+ version: number | null;
104
+ } | {
105
+ ok: false;
106
+ reason: "not-found" | "version-changed";
107
+ }>;
108
+ /** Structural reference-index row: the host owns extraction and persistence. */
109
+ export interface EntryRefRow {
110
+ readonly workspaceId: UUID;
111
+ readonly sourceEntryId: UUID;
112
+ readonly sourceKind: "widget-area-placement" | "widget-embed" | "config-field" | "page-html-embed";
113
+ readonly fieldPath: string;
114
+ readonly targetKind: "entry" | "term" | "asset";
115
+ readonly targetId: UUID;
116
+ }
117
+ /** The one reference-index operation widgets performs; no extractor implementation is copied. */
118
+ export interface EntryRefsRepoPort {
119
+ replaceForSource(required: {
120
+ workspaceId: UUID;
121
+ sourceEntryId: UUID;
122
+ refs: readonly EntryRefRow[];
123
+ }): Promise<void>;
124
+ findBySource(required: {
125
+ workspaceId: UUID;
126
+ sourceEntryId: UUID;
127
+ }): Promise<EntryRefRow[]>;
128
+ findByTarget(required: {
129
+ workspaceId: UUID;
130
+ targetKind: EntryRefRow["targetKind"];
131
+ targetId: UUID;
132
+ }): Promise<EntryRefRow[]>;
133
+ removeBySource(required: {
134
+ workspaceId: UUID;
135
+ sourceEntryId: UUID;
136
+ }): Promise<void>;
137
+ rebuildForWorkspace(required: {
138
+ workspaceId: UUID;
139
+ refs: readonly EntryRefRow[];
140
+ }): Promise<void>;
141
+ }
142
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @file Widget-instance READ accessors (SPEC-043 REQ-04) — `write-service.ts` only ever exported
3
+ * create/update/trash/purge; this file is the missing `widgets.read`-gated get/list side, named
4
+ * separately per `features/entries`' own `write-service.ts`/`list.ts` split convention.
5
+ *
6
+ * External /audit-work finding (2026-07-21, ADR-047): confirmed no read/list accessor existed in
7
+ * this package at all — the admin `get-by-id`/`list` routes (`routes/admin/widgets/*.ts`) this
8
+ * dispatch builds depend on it directly.
9
+ *
10
+ * Disclosed, not silently omitted: REQ-04 also asks for "its full revision history." No revision-
11
+ * READ capability exists anywhere in `features/entries` today for ANY content type (confirmed:
12
+ * `EntryRepoPort` only exposes `appendRevision`, never a corresponding list/read method — neither
13
+ * `InMemoryEntryRepo` nor `SqliteEntryRepo` exposes one either, even as an extra, non-port method).
14
+ * That is a real, pre-existing gap in the shared chokepoint, not a widgets-specific hole, and
15
+ * meaningfully larger than the two additive fixes this dispatch already made to
16
+ * `features/entries/write-service.ts` (the `onWritten` hook, the `bodyJson` parameter) — it needs
17
+ * its own port + both adapters, a bigger shared-module change than this dispatch is authorized to
18
+ * make unilaterally. `getWidgetInstance` below returns current state only; `revisions: []` is
19
+ * returned as an honest placeholder shape (not a silently-dropped field) so callers/tests have a
20
+ * stable contract to code against once the underlying capability lands.
21
+ *
22
+ * Architectural role:
23
+ * `widgets` domain logic, read side.
24
+ */
25
+ import type { UUID } from "@jini-ai/core/primitives";
26
+ import type { EntryListPort, EntryRepoPort } from "../entries/index.js";
27
+ import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
28
+ import type { WidgetInstanceEntry } from "./types.js";
29
+ export interface WidgetReadServiceDeps {
30
+ entryRepo: EntryRepoPort & EntryListPort;
31
+ authorize: WidgetsAuthorizeFn;
32
+ }
33
+ export interface GetWidgetInstanceInput {
34
+ readonly workspaceId: UUID;
35
+ readonly actor: {
36
+ readonly principalId: UUID;
37
+ };
38
+ readonly widgetInstanceId: UUID;
39
+ }
40
+ export interface GetWidgetInstanceRequired {
41
+ deps: WidgetReadServiceDeps;
42
+ input: GetWidgetInstanceInput;
43
+ }
44
+ /** REQ-04: read a widget instance's current state (+ revision history — see this file's header for
45
+ * the disclosed gap on that half). Returns any status (`active`/`trash`/`purged`) — the caller
46
+ * decides whether a non-`active` instance is presentable (e.g. an admin editor still shows a
47
+ * trashed instance so it can be restored/inspected).
48
+ *
49
+ * `widgetInstanceId` is resolved slug-first, id-second — same order/rationale as posts'
50
+ * `getAdminPostByIdOrSlug` (`src/features/post/post.ts`) and forms'
51
+ * `resolveFormDefinitionByIdOrSlug` (`server/inbound/admin-http/routes/forms/resolve-definition.ts`):
52
+ * the admin widget editor's URL now carries the widget's slug (`WidgetsLibrary.tsx`'s row link,
53
+ * `use-widget-instance-editor.hooks.ts`'s create-navigate), so this param may itself BE that slug.
54
+ * An old id-based bookmark/link still resolves via the `findById` fallback. */
55
+ export declare function getWidgetInstance(required: GetWidgetInstanceRequired, _optional?: Record<string, never>): Promise<{
56
+ instance: WidgetInstanceEntry;
57
+ revisions: [];
58
+ }>;
59
+ export interface ListWidgetInstancesInput {
60
+ readonly workspaceId: UUID;
61
+ readonly actor: {
62
+ readonly principalId: UUID;
63
+ };
64
+ /** Narrow to one widget type; omitted lists every type (mirrors `EntryListPort.listByWorkspace`'s
65
+ * own optional-`type` shape). */
66
+ readonly widgetType?: string;
67
+ /** Excludes `trash`/`purged` instances by default — the library screen's default view (REQ-04
68
+ * read access still covers reading a specific trashed instance directly via `getWidgetInstance`,
69
+ * this is only the list's default filter). */
70
+ readonly includeInactive?: boolean;
71
+ }
72
+ export interface ListWidgetInstancesRequired {
73
+ deps: WidgetReadServiceDeps;
74
+ input: ListWidgetInstancesInput;
75
+ }
76
+ export declare function listWidgetInstances(required: ListWidgetInstancesRequired, _optional?: Record<string, never>): Promise<{
77
+ instances: WidgetInstanceEntry[];
78
+ skippedCount: number;
79
+ skippedIds: string[];
80
+ }>;
81
+ //# sourceMappingURL=read-service.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { requireWidgetPermission } from "./authorize-helper.js";
2
+ import { WidgetInstanceNotFoundError } from "./errors.js";
3
+ import { toWidgetInstanceEntry } from "./entry-payload.js";
4
+ import { WIDGET_CONTENT_TYPE } from "./types.js";
5
+ /** REQ-04: read a widget instance's current state (+ revision history — see this file's header for
6
+ * the disclosed gap on that half). Returns any status (`active`/`trash`/`purged`) — the caller
7
+ * decides whether a non-`active` instance is presentable (e.g. an admin editor still shows a
8
+ * trashed instance so it can be restored/inspected).
9
+ *
10
+ * `widgetInstanceId` is resolved slug-first, id-second — same order/rationale as posts'
11
+ * `getAdminPostByIdOrSlug` (`src/features/post/post.ts`) and forms'
12
+ * `resolveFormDefinitionByIdOrSlug` (`server/inbound/admin-http/routes/forms/resolve-definition.ts`):
13
+ * the admin widget editor's URL now carries the widget's slug (`WidgetsLibrary.tsx`'s row link,
14
+ * `use-widget-instance-editor.hooks.ts`'s create-navigate), so this param may itself BE that slug.
15
+ * An old id-based bookmark/link still resolves via the `findById` fallback. */
16
+ export async function getWidgetInstance(required, _optional = {}) {
17
+ const { deps, input } = required;
18
+ await requireWidgetPermission({
19
+ authorize: deps.authorize,
20
+ actor: input.actor,
21
+ workspaceId: input.workspaceId,
22
+ permission: "widgets.read",
23
+ });
24
+ const bySlug = await deps.entryRepo.findBySlug({
25
+ workspaceId: input.workspaceId,
26
+ type: WIDGET_CONTENT_TYPE,
27
+ slug: input.widgetInstanceId.trim().toLowerCase(),
28
+ });
29
+ const entry = bySlug ?? (await deps.entryRepo.findById({ workspaceId: input.workspaceId, id: input.widgetInstanceId }));
30
+ if (!entry || entry.type !== WIDGET_CONTENT_TYPE) {
31
+ throw new WidgetInstanceNotFoundError({ message: `widget instance '${input.widgetInstanceId}' was not found` });
32
+ }
33
+ return { instance: toWidgetInstanceEntry({ entry: entry }), revisions: [] };
34
+ }
35
+ export async function listWidgetInstances(required, _optional = {}) {
36
+ const { deps, input } = required;
37
+ await requireWidgetPermission({
38
+ authorize: deps.authorize,
39
+ actor: input.actor,
40
+ workspaceId: input.workspaceId,
41
+ permission: "widgets.read",
42
+ });
43
+ const rows = await deps.entryRepo.listByWorkspace({ workspaceId: input.workspaceId }, { type: WIDGET_CONTENT_TYPE });
44
+ const instances = [];
45
+ let skippedCount = 0;
46
+ const skippedIds = [];
47
+ for (const row of rows) {
48
+ let instance;
49
+ try {
50
+ instance = toWidgetInstanceEntry({ entry: row });
51
+ }
52
+ catch {
53
+ // A malformed widget-instance row (e.g. wiped by an unrelated generic-entry update that
54
+ // bypassed this feature's own write path) is skipped, not a 500 for the whole admin library
55
+ // screen (Fable adversarial-review fix, 2026-07-21, Finding B).
56
+ //
57
+ // 2026-08-03 (dossier C5 follow-up): the skip itself was silent — nothing recorded that a row
58
+ // never reached the caller, so a genuinely corrupted widget could vanish from the library with
59
+ // zero trace. Kept the skip (a malformed row must not 500 the whole screen), added a
60
+ // server-side log line plus returned count and IDs so it's observable instead of invisible.
61
+ // Deliberately logs only `row.id`/`workspaceId`, never `row.fieldsJson` — a malformed payload
62
+ // may hold arbitrary caller-supplied content, and this line is not the place to disclose it.
63
+ console.warn("[widgets] listWidgetInstances: skipping malformed widget-instance row", {
64
+ entryId: row.id,
65
+ workspaceId: input.workspaceId,
66
+ });
67
+ skippedCount += 1;
68
+ skippedIds.push(row.id);
69
+ continue;
70
+ }
71
+ if (input.widgetType && instance.widgetType !== input.widgetType)
72
+ continue;
73
+ if (!input.includeInactive && instance.status !== "active")
74
+ continue;
75
+ instances.push(instance);
76
+ }
77
+ return { instances, skippedCount, skippedIds };
78
+ }
79
+ //# sourceMappingURL=read-service.js.map
@@ -0,0 +1,110 @@
1
+ import type { WidgetHostPorts } from "./host-ports.js";
2
+ /**
3
+ * @file `widget_area` entry CRUD + whole-document placement mutation + the derived binding-table
4
+ * reconcile step (SPEC-043 REQ-11..17; ADR-047 Debate Fold-In Amendment 1).
5
+ *
6
+ * Purpose:
7
+ * Deliberately mirrors `navigation/reconcile.ts` — same reconcile-not-author discipline for
8
+ * `widget_region_bindings` that `nav_location_bindings` already established.
9
+ * `reconcileWidgetRegionBindings` is the ONLY code path permitted to write the binding table
10
+ * (INV-02); no other file in this package calls `WidgetRegionBindingRepoPort.upsert`/`markInactive`
11
+ * outside `bindWidgetArea`/`mutateWidgetAreaPlacements`, which both delegate their binding-table
12
+ * writes through this same discipline.
13
+ *
14
+ * `entry_refs` extraction (INV-06) runs inside the same DB transaction as the triggering
15
+ * `createEntry`/`updateEntry` call, via `deps.onWritten` — see `write-service.ts`'s file header for
16
+ * the full reasoning (identical here).
17
+ *
18
+ * Architectural role:
19
+ * `widgets` domain logic (implementation outline C-006).
20
+ */
21
+ import type { Clock as ClockPort, UUID } from "@jini-ai/core/primitives";
22
+ import type { OutboxPort } from "../core/index.js";
23
+ import type { EntryRefsRepoPort } from "./ports.js";
24
+ import type { ContentTypeRepoPort } from "../content-types/index.js";
25
+ import type { EntryListPort, EntryRepoPort } from "../entries/index.js";
26
+ import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
27
+ import type { WidgetRegionBindingRepoPort } from "./ports.js";
28
+ import type { WidgetAreaEntry, WidgetPlacementNode, WidgetRegionKey } from "./types.js";
29
+ export interface RegionAreaServiceDeps {
30
+ host: Pick<WidgetHostPorts, "entries" | "contentTypes" | "entryRefs">;
31
+ entryRepo: EntryRepoPort & EntryListPort;
32
+ contentTypeRepo: ContentTypeRepoPort;
33
+ entryRefsRepo: EntryRefsRepoPort;
34
+ bindingRepo: WidgetRegionBindingRepoPort;
35
+ clock: ClockPort;
36
+ ids: {
37
+ newId: () => string;
38
+ };
39
+ authorize: WidgetsAuthorizeFn;
40
+ outbox: OutboxPort;
41
+ }
42
+ export interface BindWidgetAreaInput {
43
+ readonly workspaceId: UUID;
44
+ readonly regionKey: WidgetRegionKey;
45
+ }
46
+ export interface BindWidgetAreaRequired {
47
+ deps: RegionAreaServiceDeps;
48
+ input: BindWidgetAreaInput;
49
+ }
50
+ /**
51
+ * REQ-13: seeds a `widget_area` entry for a region with no existing binding (called on theme
52
+ * activation — a system/boot operation, not a user-initiated `widgets.place` mutation, so no
53
+ * `authorize()` gate here, matching how `navigation`'s location seeding is likewise boot-driven).
54
+ * Idempotent: a region already bound (or an existing-but-unbound `widget_area` entry for that
55
+ * region key, found via its deterministic slug) is reused, never duplicated.
56
+ */
57
+ export declare function bindWidgetArea(required: BindWidgetAreaRequired, _optional?: Record<string, never>): Promise<{
58
+ areaEntry: WidgetAreaEntry;
59
+ }>;
60
+ export interface MutateWidgetAreaPlacementsInput {
61
+ readonly workspaceId: UUID;
62
+ readonly actor: {
63
+ readonly principalId: UUID;
64
+ };
65
+ readonly areaEntryId: UUID;
66
+ readonly baseVersion: number;
67
+ /** The complete resulting ordered list — always a whole-document write, never a per-row patch (INV-03). */
68
+ readonly placements: readonly WidgetPlacementNode[];
69
+ }
70
+ export interface MutateWidgetAreaPlacementsRequired {
71
+ deps: RegionAreaServiceDeps;
72
+ input: MutateWidgetAreaPlacementsInput;
73
+ }
74
+ /** REQ-16/AC-10: every referenced widgetEntryId must exist, be a live (non-trashed) widget instance,
75
+ * in this same workspace — checked before any write. Throws the first violation found. */
76
+ export declare function validatePlacementWidgetsExist({ deps, workspaceId, placements }: {
77
+ deps: RegionAreaServiceDeps;
78
+ workspaceId: UUID;
79
+ placements: readonly WidgetPlacementNode[];
80
+ }, _optional?: Record<string, never>): Promise<void>;
81
+ /** REQ-15/16: one atomic, version-guarded write; rejects placements referencing a nonexistent/trashed/cross-workspace widget. */
82
+ export declare function mutateWidgetAreaPlacements(required: MutateWidgetAreaPlacementsRequired, _optional?: Record<string, never>): Promise<{
83
+ areaEntry: WidgetAreaEntry;
84
+ }>;
85
+ export interface ReconcileWidgetRegionBindingsInput {
86
+ readonly workspaceId: UUID;
87
+ }
88
+ export interface ReconcileWidgetRegionBindingsRequired {
89
+ deps: RegionAreaServiceDeps;
90
+ input: ReconcileWidgetRegionBindingsInput;
91
+ }
92
+ /**
93
+ * REQ-12/14: reconciles `widget_region_bindings` from the authoritative `widget_area` entries. The
94
+ * sole write path for the binding table (INV-02) — a full rescan-and-replace, exactly mirroring
95
+ * `navigation/reconcile.ts`'s `rebuildNavLocationBindings`. A region key present on more than one
96
+ * `widget_area` entry (a data-drift edge case the invariant should otherwise prevent) resolves
97
+ * last-writer-wins by iteration order, consistent with `WidgetRegionBindingRepoPort.upsert`'s own
98
+ * semantics elsewhere in this library.
99
+ *
100
+ * Orphan handling (REQ-14): this reconcile pass only ever WRITES rows implied by live `widget_area`
101
+ * entries — it never independently marks a row inactive for a region no theme declares anymore
102
+ * (that requires theme-declaration data this function is not given). `markInactive` is `widgets/
103
+ * repo.{memory,sqlite}.ts`'s own export for that call site (out of this task's scope — no HTTP/
104
+ * theme-activation route exists yet to drive it).
105
+ *
106
+ * @complexity O(n) over the workspace's `widget_area` entry count.
107
+ * @overallScore 100
108
+ */
109
+ export declare function reconcileWidgetRegionBindings(required: ReconcileWidgetRegionBindingsRequired, _optional?: Record<string, never>): Promise<void>;
110
+ //# sourceMappingURL=region-area-service.d.ts.map