@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,180 @@
1
+ import { nowIso as clockNowIso } from "@jini-ai/core/primitives";
2
+ import { PRE_AUTHORIZED, requireWidgetPermission, WIDGETS_SYSTEM_ACTOR_ID, } from "./authorize-helper.js";
3
+ import { withEntryLock } from "./concurrency.js";
4
+ import { areaDocWithPlacements, buildWidgetAreaFieldsJson, emptyWidgetAreaDoc, ensureWidgetContentTypesRegistered, parseWidgetAreaPayload, parseWidgetInstancePayload, toWidgetAreaEntry, widgetAreaSlug, } from "./entry-payload.js";
5
+ import { WidgetAreaConflictError, WidgetAreaNotFoundError, WidgetInstanceNotFoundError, } from "./errors.js";
6
+ import { WIDGET_AREA_CONTENT_TYPE, WIDGET_AREA_FIELD_NAMESPACE, WIDGET_CONTENT_TYPE, } from "./types.js";
7
+ async function extractAndStoreAreaRefs(deps, workspaceId, entry) {
8
+ const payload = parseWidgetAreaPayload({ fieldsJson: entry.fieldsJson });
9
+ const refs = deps.host.entryRefs.extract({
10
+ workspaceId,
11
+ sourceEntryId: entry.id,
12
+ sourceEntryType: WIDGET_AREA_CONTENT_TYPE,
13
+ bodyJson: payload.doc,
14
+ fieldsExt: { [WIDGET_AREA_FIELD_NAMESPACE]: { regionKey: payload.regionKey } },
15
+ });
16
+ await deps.entryRefsRepo.replaceForSource({ workspaceId, sourceEntryId: entry.id, refs });
17
+ }
18
+ /**
19
+ * The one shared shape both `createEntry` (`bindWidgetArea`) and `updateEntry`
20
+ * (`mutateWidgetAreaPlacements`) below need. Bridges `deps.outbox` (the raw, full-`DomainEvent`
21
+ * infra port) through `toEntryOutbox` into the narrower `{enqueue({name,payload})}` shape those
22
+ * chokepoints declare locally — previously `deps.outbox` was forwarded unwrapped here, which
23
+ * compiled but threw `NOT NULL constraint failed: outbox_events.id` against the real SQLite outbox
24
+ * on every region bind/placement-mutation. Both call sites share this one helper already, so fixing
25
+ * it here fixes both at once — see `write-service.ts`'s identically-named, identically-reasoned
26
+ * helper for the full investigation pointer.
27
+ */
28
+ function entriesWriteDeps(deps, workspaceId) {
29
+ return {
30
+ entryRepo: deps.entryRepo,
31
+ contentTypeRepo: deps.contentTypeRepo,
32
+ clock: deps.clock,
33
+ ids: deps.ids,
34
+ authorize: PRE_AUTHORIZED,
35
+ outbox: deps.host.entries.toEntryOutbox({ outbox: deps.outbox, clock: deps.clock, idGen: deps.ids, workspaceId }),
36
+ onWritten: (entry) => extractAndStoreAreaRefs(deps, workspaceId, entry),
37
+ };
38
+ }
39
+ /**
40
+ * REQ-13: seeds a `widget_area` entry for a region with no existing binding (called on theme
41
+ * activation — a system/boot operation, not a user-initiated `widgets.place` mutation, so no
42
+ * `authorize()` gate here, matching how `navigation`'s location seeding is likewise boot-driven).
43
+ * Idempotent: a region already bound (or an existing-but-unbound `widget_area` entry for that
44
+ * region key, found via its deterministic slug) is reused, never duplicated.
45
+ */
46
+ export async function bindWidgetArea(required, _optional = {}) {
47
+ const { deps, input } = required;
48
+ await ensureWidgetContentTypesRegistered({ deps, workspaceId: input.workspaceId });
49
+ return withEntryLock({ key: `${input.workspaceId}::area::${input.regionKey}`, fn: async () => {
50
+ const existingBinding = await deps.bindingRepo.findByRegion({ workspaceId: input.workspaceId, regionKey: input.regionKey });
51
+ if (existingBinding) {
52
+ const boundEntry = await deps.entryRepo.findById({ workspaceId: input.workspaceId, id: existingBinding.areaEntryId });
53
+ if (boundEntry)
54
+ return { areaEntry: toWidgetAreaEntry({ entry: boundEntry }) };
55
+ }
56
+ const slug = widgetAreaSlug({ regionKey: input.regionKey });
57
+ let entry = await deps.entryRepo.findBySlug({ workspaceId: input.workspaceId, type: WIDGET_AREA_CONTENT_TYPE, slug });
58
+ if (!entry) {
59
+ const created = await deps.host.entries.createEntry({
60
+ deps: entriesWriteDeps(deps, input.workspaceId),
61
+ input: {
62
+ actorId: WIDGETS_SYSTEM_ACTOR_ID,
63
+ workspaceId: input.workspaceId,
64
+ type: WIDGET_AREA_CONTENT_TYPE,
65
+ slug,
66
+ title: `Region: ${input.regionKey}`,
67
+ fieldsJson: buildWidgetAreaFieldsJson({ payload: { regionKey: input.regionKey, doc: emptyWidgetAreaDoc({}) } }),
68
+ },
69
+ });
70
+ if (!created.ok)
71
+ throw created.error;
72
+ entry = created.value.entry;
73
+ }
74
+ await deps.bindingRepo.upsert({
75
+ workspaceId: input.workspaceId,
76
+ regionKey: input.regionKey,
77
+ areaEntryId: entry.id,
78
+ updatedAt: clockNowIso({ clock: deps.clock }),
79
+ });
80
+ return { areaEntry: toWidgetAreaEntry({ entry: entry }) };
81
+ } });
82
+ }
83
+ /** REQ-16/AC-10: every referenced widgetEntryId must exist, be a live (non-trashed) widget instance,
84
+ * in this same workspace — checked before any write. Throws the first violation found. */
85
+ export async function validatePlacementWidgetsExist({ deps, workspaceId, placements }, _optional = {}) {
86
+ for (const placement of placements) {
87
+ const widget = await deps.entryRepo.findById({ workspaceId, id: placement.widgetEntryId });
88
+ if (!widget || widget.type !== WIDGET_CONTENT_TYPE) {
89
+ throw new WidgetInstanceNotFoundError({ message: `placement references widget '${placement.widgetEntryId}', which does not exist in workspace '${workspaceId}' (REQ-16)` });
90
+ }
91
+ const widgetPayload = parseWidgetInstancePayload({ fieldsJson: widget.fieldsJson });
92
+ if (widgetPayload.status === "trash" || widgetPayload.status === "purged") {
93
+ throw new WidgetInstanceNotFoundError({ message: `placement references widget '${placement.widgetEntryId}', which is trashed (REQ-16)` });
94
+ }
95
+ }
96
+ }
97
+ /** Converts a rejected `updateEntry` result into the matching typed error — a version conflict
98
+ * reports the CURRENT (post-conflict) version, any other failure propagates as-is. Never returns. */
99
+ async function rejectMutateWidgetAreaPlacementsWrite(deps, workspaceId, areaEntryId, current, error) {
100
+ if (deps.host.entries.isVersionConflict(error)) {
101
+ const latest = await deps.entryRepo.findById({ workspaceId, id: areaEntryId });
102
+ throw new WidgetAreaConflictError({ message: error.message, currentVersion: latest?.version ?? current.version });
103
+ }
104
+ throw error;
105
+ }
106
+ /** REQ-15/16: one atomic, version-guarded write; rejects placements referencing a nonexistent/trashed/cross-workspace widget. */
107
+ export async function mutateWidgetAreaPlacements(required, _optional = {}) {
108
+ const { deps, input } = required;
109
+ await requireWidgetPermission({
110
+ authorize: deps.authorize,
111
+ actor: input.actor,
112
+ workspaceId: input.workspaceId,
113
+ permission: "widgets.place",
114
+ });
115
+ return withEntryLock({ key: `${input.workspaceId}::${input.areaEntryId}`, fn: async () => {
116
+ const current = await deps.entryRepo.findById({ workspaceId: input.workspaceId, id: input.areaEntryId });
117
+ if (!current || current.type !== WIDGET_AREA_CONTENT_TYPE) {
118
+ throw new WidgetAreaNotFoundError({ message: `widget_area '${input.areaEntryId}' was not found` });
119
+ }
120
+ await validatePlacementWidgetsExist({ deps: deps, workspaceId: input.workspaceId, placements: input.placements });
121
+ const currentPayload = parseWidgetAreaPayload({ fieldsJson: current.fieldsJson });
122
+ const nextDoc = areaDocWithPlacements({ doc: currentPayload.doc, placements: input.placements });
123
+ const result = await deps.host.entries.updateEntry({
124
+ deps: entriesWriteDeps(deps, input.workspaceId),
125
+ input: {
126
+ actorId: input.actor.principalId,
127
+ workspaceId: input.workspaceId,
128
+ id: input.areaEntryId,
129
+ fieldsJson: buildWidgetAreaFieldsJson({ payload: { regionKey: currentPayload.regionKey, doc: nextDoc } }),
130
+ expectedVersion: input.baseVersion,
131
+ },
132
+ });
133
+ if (!result.ok) {
134
+ return rejectMutateWidgetAreaPlacementsWrite(deps, input.workspaceId, input.areaEntryId, current, result.error);
135
+ }
136
+ // regionKey never changes on a placement mutation — refresh the derived row's audit timestamp
137
+ // only (INV-02 still trivially holds: the binding is still exactly what a rebuild would produce).
138
+ await deps.bindingRepo.upsert({
139
+ workspaceId: input.workspaceId,
140
+ regionKey: currentPayload.regionKey,
141
+ areaEntryId: result.value.entry.id,
142
+ updatedAt: clockNowIso({ clock: deps.clock }),
143
+ });
144
+ return { areaEntry: toWidgetAreaEntry({ entry: result.value.entry }) };
145
+ } });
146
+ }
147
+ /**
148
+ * REQ-12/14: reconciles `widget_region_bindings` from the authoritative `widget_area` entries. The
149
+ * sole write path for the binding table (INV-02) — a full rescan-and-replace, exactly mirroring
150
+ * `navigation/reconcile.ts`'s `rebuildNavLocationBindings`. A region key present on more than one
151
+ * `widget_area` entry (a data-drift edge case the invariant should otherwise prevent) resolves
152
+ * last-writer-wins by iteration order, consistent with `WidgetRegionBindingRepoPort.upsert`'s own
153
+ * semantics elsewhere in this library.
154
+ *
155
+ * Orphan handling (REQ-14): this reconcile pass only ever WRITES rows implied by live `widget_area`
156
+ * entries — it never independently marks a row inactive for a region no theme declares anymore
157
+ * (that requires theme-declaration data this function is not given). `markInactive` is `widgets/
158
+ * repo.{memory,sqlite}.ts`'s own export for that call site (out of this task's scope — no HTTP/
159
+ * theme-activation route exists yet to drive it).
160
+ *
161
+ * @complexity O(n) over the workspace's `widget_area` entry count.
162
+ * @overallScore 100
163
+ */
164
+ export async function reconcileWidgetRegionBindings(required, _optional = {}) {
165
+ const { deps, input } = required;
166
+ const areaEntries = await deps.entryRepo.listByWorkspace({ workspaceId: input.workspaceId }, { type: WIDGET_AREA_CONTENT_TYPE });
167
+ const now = clockNowIso({ clock: deps.clock });
168
+ const byRegion = new Map();
169
+ for (const entry of areaEntries) {
170
+ const payload = parseWidgetAreaPayload({ fieldsJson: entry.fieldsJson });
171
+ byRegion.set(payload.regionKey, {
172
+ workspaceId: input.workspaceId,
173
+ regionKey: payload.regionKey,
174
+ areaEntryId: entry.id,
175
+ updatedAt: now,
176
+ });
177
+ }
178
+ await deps.bindingRepo.rebuildForWorkspace({ workspaceId: input.workspaceId, bindings: [...byRegion.values()] });
179
+ }
180
+ //# sourceMappingURL=region-area-service.js.map
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @file The v1 widget-type registry (SPEC-043 REQ-07..10, ADR-047 Debate Fold-In Amendment 3).
3
+ *
4
+ * Purpose:
5
+ * Each `WidgetTypeRegistration` below is plain, JSON-serializable data — schema, capability
6
+ * class, placement contexts, cost clamps, and (for dynamic types) a `resolverId` string. No
7
+ * registration record imports or references executable behavior directly; `resolverId` is a
8
+ * pointer that only ever resolves through `resolvers/index.ts`'s closed `CORE_RESOLVERS` map
9
+ * (REQ-08) — never a dynamic import, `eval`, or arbitrary function reference. This is the split
10
+ * Amendment 3 requires: type *registration* is data (Tier-1-safe, this file); type *behavior* is
11
+ * core-owned code in v1, ADR-024 Tier-2/3-gated for any future plugin-contributed dynamic type
12
+ * (`resolvers/`).
13
+ *
14
+ * `getWidgetTypeRegistration`/`findWidgetTypeRegistration` are the two lookup accessors over this
15
+ * data — pure, O(1) functions with no I/O and no reference to resolver code, kept here (unlike
16
+ * `forms/manifest.ts`'s stricter zero-function convention, which exists specifically for a
17
+ * hypothetical future loader retrofit this file has no equivalent of) because every other registry
18
+ * in this codebase (`identity/permissions.ts`) pairs its data with a plain accessor the same way.
19
+ * The split is deliberate, not incidental: `getWidgetTypeRegistration` is total and only accepts a
20
+ * `typeKey` the type system has actually proven is one of the five registered keys; a caller
21
+ * holding a merely-asserted `WidgetTypeKey` (cast from an HTTP body or decoded JSON, not narrowed)
22
+ * must use the partial `findWidgetTypeRegistration` instead — see each function's own doc.
23
+ *
24
+ * How it relates to the project:
25
+ * - Read by `resolvers/index.ts` (dispatch), `write-service.ts` (config validation, REQ-02/03),
26
+ * and any admin/AI surface that needs to know what a widget type declares.
27
+ * - Static/type-only until the registry itself is imported and iterated — no side effects at
28
+ * module load time.
29
+ */
30
+ import type { WidgetTypeKey, WidgetTypeRegistration } from "./types.js";
31
+ /**
32
+ * The complete v1 widget-type registry (REQ-09), keyed by the closed `WidgetTypeKey` union rather
33
+ * than held as an array. Declared with `satisfies` rather than a `: Readonly<Record<...>>`
34
+ * annotation — a `:` annotation would widen every entry back to the shared `WidgetTypeRegistration`
35
+ * interface (losing e.g. `RECENT_ENTRIES_REGISTRATION`'s own non-optional `clamps.maxItems`);
36
+ * `satisfies` checks the same shape constraint (every `WidgetTypeKey` has an entry, which is what
37
+ * makes `getWidgetTypeRegistration` below total) while keeping each entry's own literal type intact.
38
+ * Iterate with `Object.values(...)` where the whole table is needed (e.g. `agent-tools.ts`'s
39
+ * published-type-list).
40
+ */
41
+ export declare const WIDGET_TYPE_REGISTRATIONS: {
42
+ text: {
43
+ typeKey: "text";
44
+ capability: "static";
45
+ configSchema: {
46
+ type: string;
47
+ properties: {
48
+ body: {
49
+ type: string;
50
+ };
51
+ };
52
+ required: string[];
53
+ additionalProperties: false;
54
+ };
55
+ placementContexts: ("region" | "inline")[];
56
+ clamps: {
57
+ timeoutMs: number;
58
+ };
59
+ };
60
+ "social-links": {
61
+ typeKey: "social-links";
62
+ capability: "static";
63
+ configSchema: {
64
+ type: string;
65
+ properties: {
66
+ links: {
67
+ type: string;
68
+ items: {
69
+ type: string;
70
+ properties: {
71
+ platform: {
72
+ type: string;
73
+ };
74
+ url: {
75
+ type: string;
76
+ };
77
+ };
78
+ required: string[];
79
+ additionalProperties: false;
80
+ };
81
+ maxItems: number;
82
+ };
83
+ };
84
+ required: string[];
85
+ additionalProperties: false;
86
+ };
87
+ placementContexts: ("region" | "inline")[];
88
+ clamps: {
89
+ timeoutMs: number;
90
+ };
91
+ };
92
+ "recent-entries": {
93
+ typeKey: "recent-entries";
94
+ capability: "query";
95
+ configSchema: {
96
+ type: string;
97
+ properties: {
98
+ maxItems: {
99
+ type: string;
100
+ minimum: number;
101
+ maximum: number;
102
+ };
103
+ categoryTermId: {
104
+ type: string;
105
+ "x-ref-target": string;
106
+ };
107
+ collection: {
108
+ type: string;
109
+ };
110
+ where: {
111
+ type: string;
112
+ };
113
+ sort: {
114
+ type: string;
115
+ };
116
+ fields: {
117
+ type: string;
118
+ maxItems: number;
119
+ items: {
120
+ type: string;
121
+ };
122
+ };
123
+ layout: {
124
+ type: string;
125
+ };
126
+ columns: {
127
+ type: string;
128
+ minimum: number;
129
+ maximum: number;
130
+ };
131
+ structuredData: {
132
+ type: string;
133
+ };
134
+ };
135
+ required: string[];
136
+ additionalProperties: false;
137
+ };
138
+ placementContexts: ("region" | "inline")[];
139
+ clamps: {
140
+ maxItems: number;
141
+ timeoutMs: number;
142
+ };
143
+ resolverId: string;
144
+ };
145
+ menu: {
146
+ typeKey: "menu";
147
+ capability: "entry-reference";
148
+ configSchema: {
149
+ type: string;
150
+ properties: {
151
+ menuRef: {
152
+ type: string;
153
+ "x-ref-target": string;
154
+ };
155
+ };
156
+ required: string[];
157
+ additionalProperties: false;
158
+ };
159
+ placementContexts: ("region" | "inline")[];
160
+ clamps: {
161
+ timeoutMs: number;
162
+ };
163
+ resolverId: string;
164
+ };
165
+ "contact-form": {
166
+ typeKey: "contact-form";
167
+ capability: "form";
168
+ configSchema: {
169
+ type: string;
170
+ properties: {
171
+ formDefinitionId: {
172
+ type: string;
173
+ "x-ref-target": string;
174
+ };
175
+ successMessage: {
176
+ type: string;
177
+ };
178
+ };
179
+ required: string[];
180
+ additionalProperties: false;
181
+ };
182
+ placementContexts: ("region" | "inline")[];
183
+ clamps: {
184
+ timeoutMs: number;
185
+ };
186
+ resolverId: string;
187
+ };
188
+ };
189
+ /**
190
+ * The total lookup accessor — pure, O(1), no I/O. Only valid for a `typeKey` already narrowed to
191
+ * `WidgetTypeKey` by the type system itself (a literal, or a value whose type was proven, not
192
+ * asserted). For a raw/untrusted string that merely claims to be a `WidgetTypeKey` (an HTTP body
193
+ * field cast at the boundary, e.g. `server/routes/admin/widgets/create.ts`, or a value decoded out
194
+ * of stored JSON via `entry-payload.ts`'s `parseWidgetInstancePayload`), `undefined` is genuinely
195
+ * reachable — use `findWidgetTypeRegistration` instead; do not smuggle an unverified string past
196
+ * this signature with a cast.
197
+ *
198
+ * Generic over the specific key, rather than fixed to the `WidgetTypeRegistration` interface, so a
199
+ * literal call site (`getWidgetTypeRegistration("recent-entries")`) gets back
200
+ * `WIDGET_TYPE_REGISTRATIONS`'s own entry type for that key — e.g. with `clamps.maxItems: number`,
201
+ * not the interface's shared `maxItems?: number` — instead of forcing an artificial fallback
202
+ * (`?? 20`) that coverage could never actually exercise for that one call site.
203
+ */
204
+ export declare function getWidgetTypeRegistration<K extends WidgetTypeKey>({ typeKey }: {
205
+ typeKey: K;
206
+ }, _optional?: Record<string, never>): (typeof WIDGET_TYPE_REGISTRATIONS)[K];
207
+ /**
208
+ * The partial lookup accessor for a raw, untrusted string that has not been proven to be a
209
+ * `WidgetTypeKey` — only asserted to be one at some upstream boundary (an HTTP body field, or a
210
+ * value decoded out of stored JSON). Returns `undefined` for any string that isn't one of the five
211
+ * registered keys, which callers at those boundaries must handle for real (see
212
+ * `write-service.ts`'s `WidgetTypeUnregisteredError`, `resolvers/index.ts`'s `"unknown-type"`).
213
+ */
214
+ export declare function findWidgetTypeRegistration({ raw }: {
215
+ raw: string;
216
+ }, _optional?: Record<string, never>): WidgetTypeRegistration | undefined;
217
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,172 @@
1
+ /** `text` — static, no resolver (REQ-10). A single free-form rich-text field. */
2
+ const TEXT_REGISTRATION = {
3
+ typeKey: "text",
4
+ capability: "static",
5
+ configSchema: {
6
+ type: "object",
7
+ properties: { body: { type: "string" } },
8
+ required: ["body"],
9
+ additionalProperties: false,
10
+ },
11
+ placementContexts: ["region", "inline"],
12
+ clamps: { timeoutMs: 0 },
13
+ };
14
+ /** `social-links` — static, no resolver. An ordered list of `{ platform, url }` pairs. */
15
+ const SOCIAL_LINKS_REGISTRATION = {
16
+ typeKey: "social-links",
17
+ capability: "static",
18
+ configSchema: {
19
+ type: "object",
20
+ properties: {
21
+ links: {
22
+ type: "array",
23
+ items: {
24
+ type: "object",
25
+ properties: { platform: { type: "string" }, url: { type: "string" } },
26
+ required: ["platform", "url"],
27
+ additionalProperties: false,
28
+ },
29
+ maxItems: 20,
30
+ },
31
+ },
32
+ required: ["links"],
33
+ additionalProperties: false,
34
+ },
35
+ placementContexts: ["region", "inline"],
36
+ clamps: { timeoutMs: 0 },
37
+ };
38
+ /**
39
+ * `recent-entries` — dynamic, `query` capability. `maxItems` is clamped at the
40
+ * REGISTRATION level (the ceiling any instance's own config may request) AND
41
+ * re-enforced at the orchestration layer (REQ-25) — defense in depth, not
42
+ * redundant: this value is what a resolver *may* return at most; the
43
+ * orchestrator enforces it independent of the resolver's own discipline.
44
+ *
45
+ * Declared with `satisfies` rather than a `: WidgetTypeRegistration` annotation so this constant
46
+ * keeps its own literal shape — `clamps.maxItems: number`, not the shared interface's
47
+ * `maxItems?: number` — letting `getWidgetTypeRegistration("recent-entries")` (registry.ts) return
48
+ * a type that's honestly non-optional for THIS registration, without lying for the other four
49
+ * (`text`/`social-links`/`menu`/`contact-form`) that genuinely have no maxItems clamp.
50
+ *
51
+ * Collections plan R1 (2026-09-23): this widget becomes "Collection list" — it now accepts the
52
+ * same `collection`/`where`/`sort`/`fields`/`layout`/`columns` config keys the `{"type":"collection"}`
53
+ * marker does (`resolvers/recent-entries.ts` shares C1's `parseCollectionListConfig` parser for
54
+ * them). `maxItems` stays required and the type key stays `"recent-entries"` — no data migration —
55
+ * so every existing config (just `{maxItems}`, optionally `categoryTermId`) still validates and
56
+ * still means exactly what it meant before (D7): no `collection` set means "every user content
57
+ * type", `sort` defaults to `"updated"`. This write-time schema only checks shape (type/bounds);
58
+ * `parseCollectionListConfig` does the real field-name/value validation at resolve time.
59
+ */
60
+ const RECENT_ENTRIES_REGISTRATION = {
61
+ typeKey: "recent-entries",
62
+ capability: "query",
63
+ configSchema: {
64
+ type: "object",
65
+ properties: {
66
+ maxItems: { type: "integer", minimum: 1, maximum: 20 },
67
+ // REQ-32/EC-03: a taxonomy-term-target soft reference, extracted into entry_refs with
68
+ // targetKind 'term' (no safe-delete guarantee) — see core/entry-refs/extractor.ts.
69
+ categoryTermId: { type: "string", "x-ref-target": "term" },
70
+ // Collections plan R1 additions — shape-only here; `parseCollectionListConfig` (shared with
71
+ // the `collection` marker, C1) validates field names/values against the named content type.
72
+ collection: { type: "string" },
73
+ where: { type: "object" },
74
+ sort: { type: "string" },
75
+ fields: { type: "array", maxItems: 20, items: { type: "string" } },
76
+ layout: { type: "string" },
77
+ columns: { type: "integer", minimum: 1, maximum: 6 },
78
+ // AW-7: opt-in schema.org block ("faq-page"); shape-only like `layout`, parsed in public-list.ts.
79
+ structuredData: { type: "string" },
80
+ },
81
+ required: ["maxItems"],
82
+ additionalProperties: false,
83
+ },
84
+ placementContexts: ["region", "inline"],
85
+ clamps: { maxItems: 20, timeoutMs: 500 },
86
+ resolverId: "recent-entries",
87
+ };
88
+ /** `menu` — dynamic, `entry-reference` capability. Delegates entirely to `navigation/resolver.ts`. */
89
+ const MENU_REGISTRATION = {
90
+ typeKey: "menu",
91
+ capability: "entry-reference",
92
+ configSchema: {
93
+ type: "object",
94
+ // REQ-31: menuRef is an entry-target ref, extracted into entry_refs — see
95
+ // core/entry-refs/extractor.ts (matches the CONTACT_FORM_REGISTRATION 'x-ref-target' pattern
96
+ // below).
97
+ properties: { menuRef: { type: "string", "x-ref-target": "entry" } },
98
+ required: ["menuRef"],
99
+ additionalProperties: false,
100
+ },
101
+ placementContexts: ["region", "inline"],
102
+ clamps: { timeoutMs: 500 },
103
+ resolverId: "menu",
104
+ };
105
+ /**
106
+ * `contact-form` — dynamic, `form` capability. A thin adapter over `src/forms/`
107
+ * (REQ-36..39) — this registration carries no submission/rate-limit/mail
108
+ * config of its own; `formDefinitionId` is a ref-typed field extracted into
109
+ * `entry_refs` (REQ-31).
110
+ */
111
+ const CONTACT_FORM_REGISTRATION = {
112
+ typeKey: "contact-form",
113
+ capability: "form",
114
+ configSchema: {
115
+ type: "object",
116
+ properties: {
117
+ formDefinitionId: { type: "string", "x-ref-target": "entry" },
118
+ successMessage: { type: "string" },
119
+ },
120
+ required: ["formDefinitionId"],
121
+ additionalProperties: false,
122
+ },
123
+ placementContexts: ["region", "inline"],
124
+ clamps: { timeoutMs: 500 },
125
+ resolverId: "contact-form",
126
+ };
127
+ /**
128
+ * The complete v1 widget-type registry (REQ-09), keyed by the closed `WidgetTypeKey` union rather
129
+ * than held as an array. Declared with `satisfies` rather than a `: Readonly<Record<...>>`
130
+ * annotation — a `:` annotation would widen every entry back to the shared `WidgetTypeRegistration`
131
+ * interface (losing e.g. `RECENT_ENTRIES_REGISTRATION`'s own non-optional `clamps.maxItems`);
132
+ * `satisfies` checks the same shape constraint (every `WidgetTypeKey` has an entry, which is what
133
+ * makes `getWidgetTypeRegistration` below total) while keeping each entry's own literal type intact.
134
+ * Iterate with `Object.values(...)` where the whole table is needed (e.g. `agent-tools.ts`'s
135
+ * published-type-list).
136
+ */
137
+ export const WIDGET_TYPE_REGISTRATIONS = {
138
+ text: TEXT_REGISTRATION,
139
+ "social-links": SOCIAL_LINKS_REGISTRATION,
140
+ "recent-entries": RECENT_ENTRIES_REGISTRATION,
141
+ menu: MENU_REGISTRATION,
142
+ "contact-form": CONTACT_FORM_REGISTRATION,
143
+ };
144
+ /**
145
+ * The total lookup accessor — pure, O(1), no I/O. Only valid for a `typeKey` already narrowed to
146
+ * `WidgetTypeKey` by the type system itself (a literal, or a value whose type was proven, not
147
+ * asserted). For a raw/untrusted string that merely claims to be a `WidgetTypeKey` (an HTTP body
148
+ * field cast at the boundary, e.g. `server/routes/admin/widgets/create.ts`, or a value decoded out
149
+ * of stored JSON via `entry-payload.ts`'s `parseWidgetInstancePayload`), `undefined` is genuinely
150
+ * reachable — use `findWidgetTypeRegistration` instead; do not smuggle an unverified string past
151
+ * this signature with a cast.
152
+ *
153
+ * Generic over the specific key, rather than fixed to the `WidgetTypeRegistration` interface, so a
154
+ * literal call site (`getWidgetTypeRegistration("recent-entries")`) gets back
155
+ * `WIDGET_TYPE_REGISTRATIONS`'s own entry type for that key — e.g. with `clamps.maxItems: number`,
156
+ * not the interface's shared `maxItems?: number` — instead of forcing an artificial fallback
157
+ * (`?? 20`) that coverage could never actually exercise for that one call site.
158
+ */
159
+ export function getWidgetTypeRegistration({ typeKey }, _optional = {}) {
160
+ return WIDGET_TYPE_REGISTRATIONS[typeKey];
161
+ }
162
+ /**
163
+ * The partial lookup accessor for a raw, untrusted string that has not been proven to be a
164
+ * `WidgetTypeKey` — only asserted to be one at some upstream boundary (an HTTP body field, or a
165
+ * value decoded out of stored JSON). Returns `undefined` for any string that isn't one of the five
166
+ * registered keys, which callers at those boundaries must handle for real (see
167
+ * `write-service.ts`'s `WidgetTypeUnregisteredError`, `resolvers/index.ts`'s `"unknown-type"`).
168
+ */
169
+ export function findWidgetTypeRegistration({ raw }, _optional = {}) {
170
+ return Object.values(WIDGET_TYPE_REGISTRATIONS).find((registration) => registration.typeKey === raw);
171
+ }
172
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,41 @@
1
+ import type { UUID } from "@jini-ai/core/primitives";
2
+ import type { WidgetRegionBindingRepoPort } from "./ports.js";
3
+ import type { WidgetRegionBindingRow, WidgetRegionKey } from "./types.js";
4
+ /**
5
+ * @file In-memory `WidgetRegionBindingRepoPort` adapter (ADR-006 rule-of-two "one being built now"
6
+ * half — `repo.sqlite.ts` is the other), mirroring `navigation/repo.memory.ts`'s
7
+ * `InMemoryNavLocationBindingRepo` exactly: enforces `UNIQUE(workspace_id, region_key)` by storing
8
+ * at most one row per `(workspaceId, regionKey)` pair, `upsert` replacing any prior row for that
9
+ * key (last-writer-wins, same discipline `NavLocationBindingRepoPort.upsert` documents).
10
+ *
11
+ * Architectural role:
12
+ * Infrastructure adapter (in-memory). No feature logic — that is `region-area-service.ts`'s job;
13
+ * this class is a dumb, uniqueness-enforcing collection.
14
+ */
15
+ export declare class InMemoryWidgetRegionBindingRepo implements WidgetRegionBindingRepoPort {
16
+ private rows;
17
+ constructor(initialRows?: WidgetRegionBindingRow[]);
18
+ findByRegion(required: {
19
+ workspaceId: UUID;
20
+ regionKey: WidgetRegionKey;
21
+ }): Promise<WidgetRegionBindingRow | null>;
22
+ listByWorkspace(required: {
23
+ workspaceId: UUID;
24
+ }): Promise<WidgetRegionBindingRow[]>;
25
+ upsert(required: {
26
+ workspaceId: UUID;
27
+ regionKey: WidgetRegionKey;
28
+ areaEntryId: UUID;
29
+ updatedAt: string;
30
+ }): Promise<WidgetRegionBindingRow>;
31
+ /** REQ-14 — a theme switch orphaned this region key: drop the binding row, retain the underlying `widget_area` entry (untouched by this port). */
32
+ markInactive(required: {
33
+ workspaceId: UUID;
34
+ regionKey: WidgetRegionKey;
35
+ }): Promise<void>;
36
+ rebuildForWorkspace(required: {
37
+ workspaceId: UUID;
38
+ bindings: readonly WidgetRegionBindingRow[];
39
+ }): Promise<void>;
40
+ }
41
+ //# sourceMappingURL=repo.memory.d.ts.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @file In-memory `WidgetRegionBindingRepoPort` adapter (ADR-006 rule-of-two "one being built now"
3
+ * half — `repo.sqlite.ts` is the other), mirroring `navigation/repo.memory.ts`'s
4
+ * `InMemoryNavLocationBindingRepo` exactly: enforces `UNIQUE(workspace_id, region_key)` by storing
5
+ * at most one row per `(workspaceId, regionKey)` pair, `upsert` replacing any prior row for that
6
+ * key (last-writer-wins, same discipline `NavLocationBindingRepoPort.upsert` documents).
7
+ *
8
+ * Architectural role:
9
+ * Infrastructure adapter (in-memory). No feature logic — that is `region-area-service.ts`'s job;
10
+ * this class is a dumb, uniqueness-enforcing collection.
11
+ */
12
+ export class InMemoryWidgetRegionBindingRepo {
13
+ rows;
14
+ constructor(initialRows = []) {
15
+ this.rows = [...initialRows];
16
+ }
17
+ async findByRegion(required) {
18
+ return (this.rows.find((row) => row.workspaceId === required.workspaceId && row.regionKey === required.regionKey) ?? null);
19
+ }
20
+ async listByWorkspace(required) {
21
+ return this.rows.filter((row) => row.workspaceId === required.workspaceId);
22
+ }
23
+ async upsert(required) {
24
+ const row = {
25
+ workspaceId: required.workspaceId,
26
+ regionKey: required.regionKey,
27
+ areaEntryId: required.areaEntryId,
28
+ updatedAt: required.updatedAt,
29
+ };
30
+ const index = this.rows.findIndex((existing) => existing.workspaceId === required.workspaceId && existing.regionKey === required.regionKey);
31
+ if (index === -1) {
32
+ this.rows.push(row);
33
+ }
34
+ else {
35
+ this.rows[index] = row;
36
+ }
37
+ return row;
38
+ }
39
+ /** REQ-14 — a theme switch orphaned this region key: drop the binding row, retain the underlying `widget_area` entry (untouched by this port). */
40
+ async markInactive(required) {
41
+ this.rows = this.rows.filter((row) => !(row.workspaceId === required.workspaceId && row.regionKey === required.regionKey));
42
+ }
43
+ async rebuildForWorkspace(required) {
44
+ const others = this.rows.filter((row) => row.workspaceId !== required.workspaceId);
45
+ this.rows = [...others, ...required.bindings];
46
+ }
47
+ }
48
+ //# sourceMappingURL=repo.memory.js.map