@jini-ai/cms 0.5.4 → 0.5.6

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 (244) hide show
  1. package/dist/consent/express/index.d.ts +53 -0
  2. package/dist/consent/express/index.js +159 -0
  3. package/dist/consent/express/response.d.ts +21 -0
  4. package/dist/consent/express/response.js +89 -0
  5. package/dist/consent/html/client.d.ts +6 -0
  6. package/dist/consent/html/client.js +135 -0
  7. package/dist/consent/html/gate.d.ts +29 -0
  8. package/dist/consent/html/gate.js +104 -0
  9. package/dist/consent/html/index.d.ts +4 -0
  10. package/dist/consent/html/index.js +3 -0
  11. package/dist/consent/html/render.d.ts +27 -0
  12. package/dist/consent/html/render.js +62 -0
  13. package/dist/consent/html/static.d.ts +7 -0
  14. package/dist/consent/html/static.js +25 -0
  15. package/dist/consent/html/styles.d.ts +3 -0
  16. package/dist/consent/html/styles.js +22 -0
  17. package/dist/consent/index.d.ts +3 -0
  18. package/dist/consent/index.js +3 -0
  19. package/dist/consent/node/index.d.ts +47 -0
  20. package/dist/consent/node/index.js +169 -0
  21. package/dist/consent/policy.d.ts +47 -0
  22. package/dist/consent/policy.js +62 -0
  23. package/dist/consent/types.d.ts +40 -0
  24. package/dist/consent/types.js +5 -0
  25. package/dist/content-types/agent-tools.js +1 -1
  26. package/dist/content-types/tool-registrations.d.ts +2 -0
  27. package/dist/content-types/tool-registrations.js +1 -0
  28. package/dist/content-types/write-service.d.ts +2 -0
  29. package/dist/content-types/write-service.js +3 -0
  30. package/dist/forms/descriptor-rules.d.ts +9 -0
  31. package/dist/forms/descriptor-rules.js +112 -0
  32. package/dist/forms/email-format.d.ts +7 -0
  33. package/dist/forms/email-format.js +11 -0
  34. package/dist/forms/express/handler.js +4 -8
  35. package/dist/forms/field-registry.d.ts +193 -0
  36. package/dist/forms/field-registry.js +231 -0
  37. package/dist/forms/forms.d.ts +8 -5
  38. package/dist/forms/forms.js +29 -46
  39. package/dist/forms/html/html-authoring.js +63 -13
  40. package/dist/forms/html/html-submission-adapter.d.ts +5 -5
  41. package/dist/forms/html/html-submission-adapter.js +6 -13
  42. package/dist/forms/html-render.d.ts +10 -3
  43. package/dist/forms/html-render.js +116 -14
  44. package/dist/forms/index.d.ts +1 -0
  45. package/dist/forms/index.js +1 -0
  46. package/dist/forms/repo.memory.js +4 -4
  47. package/dist/forms/submission-attempts.d.ts +1 -1
  48. package/dist/forms/submission-attempts.js +13 -2
  49. package/dist/forms/submission-email-validation.js +2 -4
  50. package/dist/forms/submit-service.d.ts +3 -0
  51. package/dist/forms/submit-service.js +38 -5
  52. package/dist/forms/types.d.ts +50 -2
  53. package/dist/listing/batch.d.ts +53 -0
  54. package/dist/listing/batch.js +57 -0
  55. package/dist/listing/html.d.ts +72 -0
  56. package/dist/listing/html.js +109 -0
  57. package/dist/listing/index.d.ts +5 -0
  58. package/dist/listing/index.js +5 -0
  59. package/dist/listing/paths.d.ts +32 -0
  60. package/dist/listing/paths.js +37 -0
  61. package/dist/listing/see-more-script.d.ts +16 -0
  62. package/dist/listing/see-more-script.js +62 -0
  63. package/dist/media/content-type-sniffer.d.ts +1 -1
  64. package/dist/media/content-type-sniffer.js +15 -0
  65. package/dist/media/image-transformer.sharp.js +4 -1
  66. package/dist/media/media-service.d.ts +4 -0
  67. package/dist/media/media-service.js +8 -0
  68. package/dist/media/node.d.ts +1 -1
  69. package/dist/media/node.js +1 -1
  70. package/dist/media/transform-registry.js +1 -1
  71. package/dist/media/transform-types.d.ts +2 -0
  72. package/dist/media/transform-types.js +3 -0
  73. package/dist/media/video-frames.ffmpeg.d.ts +15 -2
  74. package/dist/media/video-frames.ffmpeg.js +28 -13
  75. package/dist/navigation/agent-tools.js +20 -3
  76. package/dist/navigation/dropdown.d.ts +41 -0
  77. package/dist/navigation/dropdown.js +85 -0
  78. package/dist/navigation/index.d.ts +5 -1
  79. package/dist/navigation/index.js +4 -0
  80. package/dist/navigation/menu-html.d.ts +31 -0
  81. package/dist/navigation/menu-html.js +53 -0
  82. package/dist/navigation/menu-service.d.ts +9 -3
  83. package/dist/navigation/menu-service.js +15 -3
  84. package/dist/navigation/tool-registrations.d.ts +5 -0
  85. package/dist/navigation/tool-registrations.js +32 -2
  86. package/dist/navigation/types.d.ts +6 -0
  87. package/dist/redirects/capture.d.ts +2 -0
  88. package/dist/redirects/capture.js +4 -0
  89. package/dist/redirects/redirects.d.ts +8 -0
  90. package/dist/redirects/redirects.js +14 -0
  91. package/dist/routing/index.d.ts +5 -0
  92. package/dist/routing/index.js +2 -0
  93. package/dist/routing/ports.d.ts +51 -0
  94. package/dist/routing/routing.d.ts +138 -0
  95. package/dist/routing/routing.js +443 -0
  96. package/dist/routing/types.d.ts +164 -0
  97. package/dist/routing/types.js +2 -0
  98. package/dist/seo/media.d.ts +23 -0
  99. package/dist/seo/media.js +30 -5
  100. package/dist/seo/page-head.js +8 -1
  101. package/dist/seo/seo.d.ts +2 -0
  102. package/dist/seo/seo.js +11 -6
  103. package/dist/seo/types.d.ts +8 -0
  104. package/dist/seo/xml-escape.d.ts +1 -3
  105. package/dist/seo/xml-escape.js +1 -6
  106. package/dist/widgets/concurrency.d.ts +27 -1
  107. package/dist/widgets/concurrency.js +12 -23
  108. package/dist/widgets/config-validation.js +6 -0
  109. package/dist/widgets/gallery-resolver.d.ts +23 -0
  110. package/dist/widgets/gallery-resolver.js +80 -0
  111. package/dist/widgets/html/gallery-render.d.ts +24 -0
  112. package/dist/widgets/html/gallery-render.js +65 -0
  113. package/dist/widgets/html/html-embeds.d.ts +4 -0
  114. package/dist/widgets/html/html-embeds.js +7 -1
  115. package/dist/widgets/html/index.d.ts +3 -0
  116. package/dist/widgets/html/index.js +3 -0
  117. package/dist/widgets/html/server-fragment.d.ts +9 -0
  118. package/dist/widgets/html/server-fragment.js +38 -0
  119. package/dist/widgets/markers/gallery.d.ts +50 -0
  120. package/dist/widgets/markers/gallery.js +164 -0
  121. package/dist/widgets/markers/html-attributes.d.ts +6 -1
  122. package/dist/widgets/markers/html-attributes.js +24 -9
  123. package/dist/widgets/markers/index.d.ts +2 -0
  124. package/dist/widgets/markers/index.js +2 -0
  125. package/dist/widgets/markers/marker.d.ts +4 -0
  126. package/dist/widgets/markers/marker.js +6 -0
  127. package/dist/widgets/markers/media-reference.d.ts +11 -0
  128. package/dist/widgets/markers/media-reference.js +11 -0
  129. package/dist/widgets/page-embed-types.d.ts +1 -1
  130. package/dist/widgets/page-embed-types.js +1 -1
  131. package/dist/widgets/registry.d.ts +144 -10
  132. package/dist/widgets/registry.js +40 -0
  133. package/dist/widgets/resolver-service.d.ts +10 -0
  134. package/dist/widgets/resolver-service.js +73 -29
  135. package/dist/widgets/resolvers/create-core-resolvers.d.ts +3 -0
  136. package/dist/widgets/resolvers/create-core-resolvers.js +2 -0
  137. package/dist/widgets/resolvers/gallery.d.ts +12 -0
  138. package/dist/widgets/resolvers/gallery.js +33 -0
  139. package/dist/widgets/resolvers/index.d.ts +1 -0
  140. package/dist/widgets/resolvers/index.js +1 -0
  141. package/dist/widgets/resolvers/menu.d.ts +8 -0
  142. package/dist/widgets/resolvers/menu.js +21 -6
  143. package/dist/widgets/types.d.ts +3 -1
  144. package/package.json +51 -6
  145. package/dist/http/settings/cms-adapter.d.ts +0 -20
  146. package/dist/http/settings/cms-adapter.js +0 -124
  147. package/dist/http/settings/contracts.d.ts +0 -135
  148. package/dist/http/settings/contracts.js +0 -2
  149. package/dist/http/settings/http.d.ts +0 -10
  150. package/dist/http/settings/http.js +0 -211
  151. package/dist/http/settings/index.d.ts +0 -5
  152. package/dist/http/settings/index.js +0 -4
  153. package/dist/http/settings/shared.d.ts +0 -68
  154. package/dist/http/settings/shared.js +0 -54
  155. package/dist/http/settings/stream.d.ts +0 -24
  156. package/dist/http/settings/stream.js +0 -147
  157. package/dist/settings/agent-tools.d.ts +0 -10
  158. package/dist/settings/agent-tools.js +0 -204
  159. package/dist/settings/agent-value-write-tools.d.ts +0 -22
  160. package/dist/settings/agent-value-write-tools.js +0 -154
  161. package/dist/settings/agent-writable-preferences.d.ts +0 -67
  162. package/dist/settings/agent-writable-preferences.js +0 -132
  163. package/dist/settings/agent-write-denylist.d.ts +0 -19
  164. package/dist/settings/agent-write-denylist.js +0 -15
  165. package/dist/settings/change-feed.d.ts +0 -83
  166. package/dist/settings/change-feed.js +0 -61
  167. package/dist/settings/definitions-dispatch.d.ts +0 -56
  168. package/dist/settings/definitions-dispatch.js +0 -86
  169. package/dist/settings/dictionaries/index.d.ts +0 -48
  170. package/dist/settings/dictionaries/index.js +0 -69
  171. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +0 -3
  172. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -27
  173. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +0 -3
  174. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -27
  175. package/dist/settings/dictionaries/settings-dialog.de.d.ts +0 -3
  176. package/dist/settings/dictionaries/settings-dialog.de.js +0 -27
  177. package/dist/settings/dictionaries/settings-dialog.es.d.ts +0 -3
  178. package/dist/settings/dictionaries/settings-dialog.es.js +0 -27
  179. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +0 -3
  180. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -27
  181. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +0 -3
  182. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -27
  183. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +0 -3
  184. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -27
  185. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +0 -3
  186. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -27
  187. package/dist/settings/dictionaries/settings-dialog.id.d.ts +0 -3
  188. package/dist/settings/dictionaries/settings-dialog.id.js +0 -27
  189. package/dist/settings/dictionaries/settings-dialog.it.d.ts +0 -3
  190. package/dist/settings/dictionaries/settings-dialog.it.js +0 -27
  191. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +0 -3
  192. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -27
  193. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +0 -3
  194. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -27
  195. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +0 -3
  196. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -27
  197. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +0 -3
  198. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -27
  199. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +0 -3
  200. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -27
  201. package/dist/settings/dictionaries/settings-dialog.th.d.ts +0 -3
  202. package/dist/settings/dictionaries/settings-dialog.th.js +0 -27
  203. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +0 -3
  204. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -27
  205. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +0 -3
  206. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -27
  207. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +0 -3
  208. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -27
  209. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +0 -3
  210. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -27
  211. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +0 -3
  212. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -27
  213. package/dist/settings/ensure-definitions.d.ts +0 -88
  214. package/dist/settings/ensure-definitions.js +0 -91
  215. package/dist/settings/errors.d.ts +0 -42
  216. package/dist/settings/errors.js +0 -46
  217. package/dist/settings/express/cms-adapter.d.ts +0 -20
  218. package/dist/settings/express/cms-adapter.js +0 -124
  219. package/dist/settings/express/contracts.d.ts +0 -135
  220. package/dist/settings/express/contracts.js +0 -2
  221. package/dist/settings/express/http.d.ts +0 -10
  222. package/dist/settings/express/http.js +0 -211
  223. package/dist/settings/express/shared.d.ts +0 -68
  224. package/dist/settings/express/shared.js +0 -54
  225. package/dist/settings/express/stream.d.ts +0 -24
  226. package/dist/settings/express/stream.js +0 -147
  227. package/dist/settings/ports.d.ts +0 -118
  228. package/dist/settings/principal-lookup.d.ts +0 -9
  229. package/dist/settings/principal-lookup.js +0 -2
  230. package/dist/settings/purge-service.d.ts +0 -54
  231. package/dist/settings/purge-service.js +0 -106
  232. package/dist/settings/repo.memory.d.ts +0 -119
  233. package/dist/settings/repo.memory.js +0 -199
  234. package/dist/settings/settings.d.ts +0 -139
  235. package/dist/settings/settings.js +0 -259
  236. package/dist/settings/tool-registrations.d.ts +0 -51
  237. package/dist/settings/tool-registrations.js +0 -250
  238. package/dist/settings/types.d.ts +0 -125
  239. package/dist/settings/types.js +0 -3
  240. package/dist/settings/ui-tab-definitions.d.ts +0 -41
  241. package/dist/settings/ui-tab-definitions.js +0 -148
  242. package/dist/settings/write-service.d.ts +0 -320
  243. package/dist/settings/write-service.js +0 -683
  244. /package/dist/{settings → routing}/ports.js +0 -0
@@ -1,4 +1,6 @@
1
1
  import { isNonArrayRecord } from "@jini-ai/core";
2
+ import { resolveGalleryEmbeds } from "./gallery-resolver.js";
3
+ import { isPlausibleMediaRefId } from "./markers/media-reference.js";
2
4
  import { resolveTaxonomyEmbeds } from "./resolvers/taxonomy.js";
3
5
  import { parseWidgetAreaPayload, parseWidgetInstancePayload } from "./entry-payload.js";
4
6
  import { resolveWidgetType } from "./resolvers/index.js";
@@ -27,7 +29,7 @@ function collectWidgetEmbeds(root, out) {
27
29
  if (!isNonArrayRecord(candidate))
28
30
  continue;
29
31
  if (isNonArrayRecord(attrs) && typeof attrs.value.widgetEntryId === "string" && typeof attrs.value.placementId === "string") {
30
- out.push({ placementId: attrs.value.placementId, widgetEntryId: attrs.value.widgetEntryId });
32
+ out.push({ placementId: attrs.value.placementId, widgetEntryId: attrs.value.widgetEntryId, ...(isNonArrayRecord({ value: attrs.value.content }) ? { content: attrs.value.content } : {}) });
31
33
  }
32
34
  if (Array.isArray(candidate.value.content))
33
35
  stack.push(candidate.value.content);
@@ -82,9 +84,19 @@ function groupWidgetInstancesByType(widgetRows, referencedIds) {
82
84
  }
83
85
  return byType;
84
86
  }
85
- async function resolveWidgetInstances(deps, workspaceId, referencedIds, context) {
87
+ async function resolveWidgetInstances(deps, workspaceId, referencedIds, context, overrides = [], originalIds = referencedIds) {
86
88
  const widgetRows = referencedIds.size > 0 ? await deps.entryRepo.listByWorkspace({ workspaceId }, { type: WIDGET_CONTENT_TYPE }) : [];
87
- const byType = groupWidgetInstancesByType(widgetRows, referencedIds);
89
+ const byType = groupWidgetInstancesByType(widgetRows, originalIds);
90
+ const rowsById = new Map(widgetRows.map(row => [row.id, row]));
91
+ for (const override of overrides) {
92
+ const row = rowsById.get(override.widgetEntryId);
93
+ const saved = row ? parseWidgetInstanceRow(row) : null;
94
+ if (!saved)
95
+ continue;
96
+ const instances = byType.get(saved.widgetType) ?? [];
97
+ instances.push({ ...saved, id: override.key, config: { ...saved.config, ...override.content } });
98
+ byType.set(saved.widgetType, instances);
99
+ }
88
100
  const resolvedById = new Map();
89
101
  for (const [typeKey, instances] of byType) {
90
102
  const results = await resolveWidgetType({ typeKey, instances, context });
@@ -154,21 +166,27 @@ export async function resolvePageWidgets(required, _optional = {}) {
154
166
  // handles them as "unresolved", not a crash), at most one `resolveWidgetType` call per distinct
155
167
  // type present (REQ-24) — see `resolveWidgetInstances`'s own doc for why this is shared rather
156
168
  // than inlined here.
157
- const resolvedById = await resolveWidgetInstances(deps, input.workspaceId, referencedIds, context);
169
+ const allPlacements = [...inlineEmbeds, ...[...regionPlacements.values()].flat()];
170
+ const originalIds = new Set(allPlacements.filter(placement => !placement.content).map(placement => placement.widgetEntryId));
171
+ const resolvedById = await resolveWidgetInstances(deps, input.workspaceId, referencedIds, context, inlineContentOverrides(allPlacements), originalIds);
158
172
  // 6. Assemble region -> IR[] (missing/failed placements degrade to the REQ-28 placeholder).
159
173
  const regions = {};
160
174
  for (const [regionKey, placements] of regionPlacements) {
161
- regions[regionKey] = placements.map((placement) => toRenderIr(resolvedById.get(placement.widgetEntryId)));
175
+ regions[regionKey] = placements.map((placement) => toRenderIr(resolvedById.get(placement.content ? placement.placementId : placement.widgetEntryId)));
162
176
  }
163
177
  // 7. Assemble placementId -> IR for inline embeds.
164
178
  return { regions, inlineResolved: assembleInlineResolved(inlineEmbeds, resolvedById) };
165
179
  }
180
+ /** Placement config merges before asset resolution; a repeated widget never shares overridden IR. */
181
+ function inlineContentOverrides(embeds) {
182
+ return embeds.flatMap(embed => embed.content ? [{ key: embed.placementId, widgetEntryId: embed.widgetEntryId, content: embed.content }] : []);
183
+ }
166
184
  /** placementId -> IR for every inline embed; a missing/trashed/failed widget degrades to the REQ-28
167
185
  * placeholder IR. Shared by {@link resolvePageWidgets} and {@link resolvePostContentWidgetContext}. */
168
186
  function assembleInlineResolved(inlineEmbeds, resolvedById) {
169
187
  const inlineResolved = new Map();
170
188
  for (const embed of inlineEmbeds) {
171
- inlineResolved.set(embed.placementId, toRenderIr(resolvedById.get(embed.widgetEntryId)));
189
+ inlineResolved.set(embed.placementId, toRenderIr(resolvedById.get(embed.content ? embed.placementId : embed.widgetEntryId)));
172
190
  }
173
191
  return inlineResolved;
174
192
  }
@@ -270,8 +288,22 @@ async function resolveWidgetTypeEmbeds(refs, deps, context) {
270
288
  const slugRefs = refs.filter((ref) => ref.id === null && ref.slug !== null);
271
289
  const idBySlug = await resolveWidgetSlugsToIds(slugRefs, deps, context);
272
290
  const ids = collectWidgetIdsToLoad(refs, idBySlug, context);
273
- const resolvedRaw = await resolveWidgetInstances(deps, context.workspaceId, ids, context);
274
- return buildResolvedWidgetMap(refs, idBySlug, resolvedRaw);
291
+ const overrides = refs.flatMap(ref => {
292
+ const content = ref.config?.content;
293
+ const widgetEntryId = ref.id ?? (ref.slug ? idBySlug.get(ref.slug) : undefined);
294
+ return widgetEntryId && ref.placementKey && isNonArrayRecord({ value: content })
295
+ ? [{ key: ref.placementKey, widgetEntryId, content: content }] : [];
296
+ });
297
+ const originalIds = new Set(refs.filter(ref => !ref.placementKey).flatMap(ref => {
298
+ const id = ref.id ?? (ref.slug ? idBySlug.get(ref.slug) : undefined);
299
+ return id ? [id] : [];
300
+ }));
301
+ const resolvedRaw = await resolveWidgetInstances(deps, context.workspaceId, ids, context, overrides, originalIds);
302
+ const resolved = buildResolvedWidgetMap(refs, idBySlug, resolvedRaw);
303
+ for (const override of overrides)
304
+ if (resolvedRaw.has(override.key))
305
+ resolved.set(override.key, toRenderIr(resolvedRaw.get(override.key)));
306
+ return resolved;
275
307
  }
276
308
  /** A form slug/id is a synthetic contact-form instance, resolved through the widget pipeline. */
277
309
  async function resolveFormTypeEmbeds(refs, _deps, context) {
@@ -283,19 +315,6 @@ async function resolveFormTypeEmbeds(refs, _deps, context) {
283
315
  });
284
316
  return new Map(keys.map((key) => [key, toRenderIr(results.get(key))]));
285
317
  }
286
- /** Mirrors `render.ts`'s own `MAX_MEDIA_REF_ID_LENGTH`/`PLAUSIBLE_MEDIA_REF_ID_PATTERN` shape check
287
- * for an `assetId`/`transformName` that will eventually become a `/m/` URL path segment —
288
- * duplicated here rather than imported for the same reason `core/entry-refs/extractor.ts`
289
- * duplicates `html-embeds.ts`'s scan pattern instead of importing it: `widgets/` (this file) must
290
- * not depend on `server/http/site/render.ts`, which sits above it in this codebase's layering. Both
291
- * copies guard the SAME hazard — this copy stops a malformed id/name from being used as a
292
- * repo-lookup key at resolve time; `render.ts`'s copy (`renderWidgetMediaImage`) re-checks
293
- * defense-in-depth immediately before URL templating. */
294
- const MAX_MEDIA_REF_ID_LENGTH = 200;
295
- const PLAUSIBLE_MEDIA_REF_ID_PATTERN = /^[^\s/]+$/;
296
- function isPlausibleMediaRefId(value) {
297
- return value.length > 0 && value.length <= MAX_MEDIA_REF_ID_LENGTH && PLAUSIBLE_MEDIA_REF_ID_PATTERN.test(value);
298
- }
299
318
  /** The IR's `slug` for a resolved asset: `null` means "has none" (`MediaAssetRenderMeta`'s own
300
319
  * contract), and a pre-backfill row stores that as `""`, so an empty slug is normalized to `null`
301
320
  * rather than passed through. Shared by every media IR builder in this file. */
@@ -318,7 +337,7 @@ function mediaIrSlug(record) {
318
337
  function parseMediaEmbedRef(ref, context, host) {
319
338
  const assetId = ref.id ?? ref.slug;
320
339
  const transformName = ref.variant ?? host.media.publicTransformName;
321
- if (assetId === null || !isPlausibleMediaRefId(assetId) || !isPlausibleMediaRefId(transformName)) {
340
+ if (assetId === null || !isPlausibleMediaRefId({ value: assetId }) || !isPlausibleMediaRefId({ value: transformName })) {
322
341
  console.warn('[widgets] resolveHtmlPageEmbeds: unresolved "media" reference — missing or invalid "id"/"slug"/"variant" in data-embed-config', { workspaceId: context.workspaceId });
323
342
  return undefined;
324
343
  }
@@ -378,8 +397,10 @@ async function resolveOneMediaEmbed(ref, mediaRepo, transformRepo, mediaContentT
378
397
  // previously-rendered HTML — that used to be `record.id`'s whole reason for being the emitted
379
398
  // value; now it is `media_slug_history` (S2a/S2b) keeping the OLD slug resolving too.
380
399
  const { assetId: refAssetId, transformName } = parsed;
381
- const record = await host.media.findMediaByIdOrSlug({ deps: { mediaRepo }, input: { workspaceId: context.workspaceId, idOrSlug: refAssetId } });
382
- if (!record) {
400
+ const record = ref.id === null && ref.slug !== null
401
+ ? await mediaRepo.findBySlug({ workspaceId: context.workspaceId, slug: ref.slug })
402
+ : await host.media.findMediaByIdOrSlug({ deps: { mediaRepo }, input: { workspaceId: context.workspaceId, idOrSlug: refAssetId } });
403
+ if (!record || record.status !== "active") {
383
404
  console.warn('[widgets] resolveHtmlPageEmbeds: unresolved "media" reference — no such asset', {
384
405
  workspaceId: context.workspaceId,
385
406
  assetId: refAssetId,
@@ -390,8 +411,8 @@ async function resolveOneMediaEmbed(ref, mediaRepo, transformRepo, mediaContentT
390
411
  const contentType = mediaContentTypeStore
391
412
  ? (await mediaContentTypeStore.getMany({ workspaceId: context.workspaceId, sha256s: [record.source.sha256] })).get(record.source.sha256)
392
413
  : undefined;
393
- if (contentType?.startsWith("video/")) {
394
- // Video needs no transform lookup at all: `render.ts`'s video branch points straight at
414
+ if (contentType?.startsWith("video/") || contentType?.startsWith("audio/")) {
415
+ // Video/audio need no image transform lookup: the shared native-player renderer points at
395
416
  // `/m/{assetId}/original` (the byte-passthrough route — see that route's own doc for why
396
417
  // video can't go through the image-transform pipeline below), so `transformName`/`version`
397
418
  // would be unused props for this IR.
@@ -431,6 +452,10 @@ async function resolveMediaTypeEmbeds(refs, deps, context) {
431
452
  }
432
453
  return resolved;
433
454
  }
455
+ /** Gallery widget adapter over the same media owner used by gallery HTML embeds. */
456
+ export function resolveGalleryWidgetMedia({ refs, deps, context }, _optional = {}) {
457
+ return resolveMediaTypeEmbeds(refs, deps, context);
458
+ }
434
459
  /** Scans a TipTap-shaped `bodyJson` tree for every ref-based `image`/`media` node's `assetId`
435
460
  * (ADR-027 §4's `{assetId, transformName}` shape) — both the legacy image-only ref shape and the
436
461
  * generic `media` node (2026-09-11, `render.ts`'s `renderDocMedia`'s own doc) share this collection,
@@ -586,7 +611,7 @@ async function resolvePostContentWidgetContext(deps, bodyJson, context) {
586
611
  if (inlineEmbeds.length === 0)
587
612
  return { inlineWidgets: {} };
588
613
  const referencedIds = new Set(inlineEmbeds.map((embed) => embed.widgetEntryId));
589
- const resolvedById = await resolveWidgetInstances(deps, context.workspaceId, referencedIds, context);
614
+ const resolvedById = await resolveWidgetInstances(deps, context.workspaceId, referencedIds, context, inlineContentOverrides(inlineEmbeds), new Set(inlineEmbeds.filter(embed => !embed.content).map(embed => embed.widgetEntryId)));
590
615
  const inlineResolved = assembleInlineResolved(inlineEmbeds, resolvedById);
591
616
  return { inlineWidgets: Object.fromEntries(inlineResolved) };
592
617
  }
@@ -845,7 +870,22 @@ export const HTML_EMBED_RESOLVERS = {
845
870
  widget: resolveWidgetTypeEmbeds,
846
871
  form: resolveFormTypeEmbeds,
847
872
  taxonomy: (refs, deps) => resolveTaxonomyEmbeds({ refs, deps }),
848
- media: resolveMediaTypeEmbeds,
873
+ media: async (refs, deps, context) => {
874
+ const inline = refs.filter(ref => ref.config?.content !== undefined || ref.config?.items !== undefined || ref.config?.html !== undefined);
875
+ const ordinary = refs.filter(ref => !inline.includes(ref));
876
+ const media = await resolveMediaTypeEmbeds(ordinary, deps, context);
877
+ const content = await resolveGalleryEmbeds({
878
+ refs: inline.map(ref => ({ ...ref, config: (ref.config?.content ?? ref.config) })),
879
+ resolveMedia: ({ refs }) => resolveMediaTypeEmbeds(refs, deps, context),
880
+ }, { reportIssue: ({ key, messages }) => console.warn('[widgets] unresolved media content', { key, messages }) });
881
+ return new Map([...media, ...content]);
882
+ },
883
+ gallery: (refs, deps, context) => resolveGalleryEmbeds({
884
+ refs,
885
+ resolveMedia: ({ refs: mediaRefs }) => resolveMediaTypeEmbeds(mediaRefs, deps, context),
886
+ }, {
887
+ reportIssue: ({ key, messages }) => console.warn('[widgets] unresolved gallery embed', { workspaceId: context.workspaceId, key, messages }),
888
+ }),
849
889
  post: resolvePostTypeEmbeds,
850
890
  content: resolveContentTypeEmbeds,
851
891
  };
@@ -898,8 +938,12 @@ export const HTML_EMBED_RESOLVERS = {
898
938
  *
899
939
  * `"featured-image"` (2026-10-05) joins for the identical reason: `static-render.ts`'s
900
940
  * `injectFeaturedImage` resolves it (see `FEATURED_IMAGE_MARKER_TYPE`, `core/embeds/marker.ts`).
941
+ *
942
+ * `"post-author"` (2026-10-11) joins for the identical reason: `static-render.ts`'s
943
+ * `injectPostAuthor` fills the byline. Without it this stage warned once per post render while the
944
+ * byline rendered correctly.
901
945
  */
902
- export const THEME_OWNED_MARKER_TYPES = new Set(["partial", "menu", "post-previews", "collection", "featured-image"]);
946
+ export const THEME_OWNED_MARKER_TYPES = new Set(["partial", "menu", "post-previews", "collection", "featured-image", "post-author"]);
903
947
  /**
904
948
  * `isPageEmbedType` (the "does this stage OWN the marker type" question, with its own doc) lives in
905
949
  * the dependency-free `page-embed-types.ts` so `render.ts` and the theme render workers can ask it
@@ -2,6 +2,7 @@ import type { EntryDisplayListPort, EntryListExcludingTypesPort, CollectionListP
2
2
  import type { EntryListPort } from "../../entries/index.js";
3
3
  import type { NavMenuReadModel, ResolveTargetHrefFn } from "../../navigation/index.js";
4
4
  import type { WidgetResolver, WidgetTypeKey } from "../types.js";
5
+ import { createGalleryWidgetResolver } from "./gallery.js";
5
6
  import { type ContentTypeLookup } from "./recent-entries.js";
6
7
  /**
7
8
  * @file Assembles the real, DI'd v1 dynamic resolvers against real infrastructure deps.
@@ -14,6 +15,8 @@ import { type ContentTypeLookup } from "./recent-entries.js";
14
15
  * test would otherwise need to construct.
15
16
  */
16
17
  export interface CoreResolverDeps {
18
+ /** Media remains host-owned. Older hosts degrade nonempty galleries until this port is wired. */
19
+ galleryMedia?: Parameters<typeof createGalleryWidgetResolver>[0]["resolveMedia"];
17
20
  menus: MenusPort;
18
21
  collectionList: CollectionListPort;
19
22
  resolveMenuTargetHref?: ResolveTargetHrefFn;
@@ -1,8 +1,10 @@
1
+ import { createGalleryWidgetResolver } from "./gallery.js";
1
2
  import { createContactFormResolver } from "./contact-form.js";
2
3
  import { createMenuResolver } from "./menu.js";
3
4
  import { createRecentEntriesResolver } from "./recent-entries.js";
4
5
  export function createCoreResolvers(deps, _optional = {}) {
5
6
  return {
7
+ gallery: createGalleryWidgetResolver({ resolveMedia: deps.galleryMedia ?? (async () => new Map()) }),
6
8
  "recent-entries": createRecentEntriesResolver({ collectionList: deps.collectionList, entryList: deps.entryList, contentTypes: deps.contentTypes }),
7
9
  menu: createMenuResolver({ menus: deps.menus, navMenuReadModel: deps.navMenuReadModel, ...(deps.resolveMenuTargetHref === undefined ? {} : { resolveTargetHref: deps.resolveMenuTargetHref }), publicOnly: true }),
8
10
  "contact-form": createContactFormResolver({ formDefinitionRepo: deps.formDefinitionRepo }),
@@ -0,0 +1,12 @@
1
+ import type { WidgetResolver } from '../types.js';
2
+ import type { GalleryMediaResolver } from '../gallery-resolver.js';
3
+ /** Adapt gallery widget instances to the existing embed resolver; one deduplicated media batch.
4
+ * Empty starter widgets render a note, while malformed stored configs degrade per instance.
5
+ * @complexity O(w*i) time/space, with i<=50 images and a shared 50-asset budget.
6
+ */
7
+ export declare function createGalleryWidgetResolver({ resolveMedia }: {
8
+ resolveMedia: (required: Parameters<GalleryMediaResolver>[0] & {
9
+ context: Parameters<WidgetResolver['resolveMany']>[1];
10
+ }, optional?: Record<string, never>) => ReturnType<GalleryMediaResolver>;
11
+ }, _optional?: Record<string, never>): WidgetResolver;
12
+ //# sourceMappingURL=gallery.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { validateWidgetConfig } from '../config-validation.js';
2
+ import { getWidgetTypeRegistration } from '../registry.js';
3
+ import { resolveGalleryEmbeds } from '../gallery-resolver.js';
4
+ import { galleryMediaReferences, parseGalleryConfig } from '../markers/gallery.js';
5
+ /** Adapt gallery widget instances to the existing embed resolver; one deduplicated media batch.
6
+ * Empty starter widgets render a note, while malformed stored configs degrade per instance.
7
+ * @complexity O(w*i) time/space, with i<=50 images and a shared 50-asset budget.
8
+ */
9
+ export function createGalleryWidgetResolver({ resolveMedia }, _optional = {}) {
10
+ return { async resolveMany(instances, context) {
11
+ const refs = [];
12
+ const results = new Map();
13
+ for (const instance of instances) {
14
+ const parsed = parseGalleryConfig({ config: instance.config }, { allowEmpty: true });
15
+ if (!parsed.config || !validateWidgetConfig({ schema: getWidgetTypeRegistration({ typeKey: instance.widgetType === 'media' ? 'media' : 'gallery' }).configSchema, config: instance.config }).valid) {
16
+ results.set(instance.id, { ok: false, reason: 'resolver-error' });
17
+ continue;
18
+ }
19
+ if (parsed.config.images.length === 0 && parsed.config.mode !== 'html') {
20
+ results.set(instance.id, { ok: true, ir: { componentId: instance.widgetType === 'media' ? 'media' : 'gallery', props: { config: instance.config, media: {} } }, dependencyKeys: [instance.id] });
21
+ }
22
+ else
23
+ refs.push({ type: instance.widgetType, id: instance.id, slug: null, name: null, variant: null, header: true, config: instance.config });
24
+ }
25
+ const resolved = await resolveGalleryEmbeds({ refs, resolveMedia: ({ refs }) => resolveMedia({ refs, context }) });
26
+ for (const ref of refs) {
27
+ const ir = resolved.get(ref.id);
28
+ results.set(ref.id, ir ? { ok: true, ir, dependencyKeys: [ref.id, ...galleryMediaReferences({ config: ref.config }).map(image => image.mediaId)] } : { ok: false, reason: 'resolver-error' });
29
+ }
30
+ return results;
31
+ } };
32
+ }
33
+ //# sourceMappingURL=gallery.js.map
@@ -21,6 +21,7 @@
21
21
  import type { UUID } from "@jini-ai/core/primitives";
22
22
  import { type CoreResolverDeps } from "./create-core-resolvers.js";
23
23
  import type { WidgetInstanceView, WidgetResolveContext, WidgetResolveResult, WidgetResolver, WidgetTypeKey } from "../types.js";
24
+ export { createGalleryWidgetResolver } from "./gallery.js";
24
25
  export { createRecentEntriesResolver } from "./recent-entries.js";
25
26
  export { createMenuResolver } from "./menu.js";
26
27
  export { createContactFormResolver } from "./contact-form.js";
@@ -6,6 +6,7 @@ import { createCoreResolvers } from "./create-core-resolvers.js";
6
6
  // isolation (2026-08-18 widgets/resolvers deep-import triage), so each factory is re-exported here
7
7
  // narrowly, one line per consumer:
8
8
  // `createRecentEntriesResolver` — `widgets/__tests__/unit/resolvers-recent-entries.unit.test.ts`.
9
+ export { createGalleryWidgetResolver } from "./gallery.js";
9
10
  export { createRecentEntriesResolver } from "./recent-entries.js";
10
11
  // `createMenuResolver` — `widgets/__tests__/unit/resolvers-menu.unit.test.ts`.
11
12
  export { createMenuResolver } from "./menu.js";
@@ -38,5 +38,13 @@ export interface MenuResolverDeps {
38
38
  /** Host routing injection; callers without it retain the diagnostic placeholder documented above. */
39
39
  resolveTargetHref?: ResolveTargetHrefFn;
40
40
  }
41
+ /**
42
+ * Resolve menu widgets through navigation's read model: stored HTML or resolved items by doc mode.
43
+ * HTML is trusted and balanced by navigation's save path (`mergeMenuHtml`), like static menus;
44
+ * forwarding it adds no second parser or sanitization policy. Read failures propagate to dispatch.
45
+ * @param deps - Navigation read/resolution ports and host routing/public-filter policy.
46
+ * @returns A resolver producing mode-specific menu IR, or existing typed config/target failures.
47
+ * @complexity One read per instance; item resolution/filtering scales with the selected item trees.
48
+ */
41
49
  export declare function createMenuResolver(deps: MenuResolverDeps, _optional?: Record<string, never>): WidgetResolver;
42
50
  //# sourceMappingURL=menu.d.ts.map
@@ -9,6 +9,14 @@ function toMenuItemProps(items, publicOnly) {
9
9
  }) : items;
10
10
  return visible;
11
11
  }
12
+ /**
13
+ * Resolve menu widgets through navigation's read model: stored HTML or resolved items by doc mode.
14
+ * HTML is trusted and balanced by navigation's save path (`mergeMenuHtml`), like static menus;
15
+ * forwarding it adds no second parser or sanitization policy. Read failures propagate to dispatch.
16
+ * @param deps - Navigation read/resolution ports and host routing/public-filter policy.
17
+ * @returns A resolver producing mode-specific menu IR, or existing typed config/target failures.
18
+ * @complexity One read per instance; item resolution/filtering scales with the selected item trees.
19
+ */
12
20
  export function createMenuResolver(deps, _optional = {}) {
13
21
  const resolveTargetHref = deps.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
14
22
  return {
@@ -25,14 +33,21 @@ export function createMenuResolver(deps, _optional = {}) {
25
33
  results.set(instance.id, { ok: false, reason: "target-disabled" });
26
34
  continue;
27
35
  }
28
- const items = await deps.menus.resolveMenuDoc({
29
- doc: menu.doc,
30
- context: { workspaceId: context.workspaceId },
31
- resolveTargetHref,
32
- });
36
+ let props;
37
+ if (menu.doc.mode === "html") {
38
+ props = { title: menu.title, mode: "html", html: menu.doc.html ?? "" };
39
+ }
40
+ else {
41
+ const items = await deps.menus.resolveMenuDoc({
42
+ doc: menu.doc,
43
+ context: { workspaceId: context.workspaceId },
44
+ resolveTargetHref,
45
+ });
46
+ props = { title: menu.title, items: toMenuItemProps(items, deps.publicOnly === true) };
47
+ }
33
48
  results.set(instance.id, {
34
49
  ok: true,
35
- ir: { componentId: "menu", props: { title: menu.title, items: toMenuItemProps(items, deps.publicOnly === true) } },
50
+ ir: { componentId: "menu", props },
36
51
  dependencyKeys: [menu.id],
37
52
  });
38
53
  }
@@ -47,7 +47,7 @@ export declare const WIDGET_AREA_FIELD_NAMESPACE: "widgets";
47
47
  * a bare `string` so a typo in a registration or a call site is a compile error,
48
48
  * not a runtime `unknown-type` surprise.
49
49
  */
50
- export type WidgetTypeKey = "text" | "social-links" | "recent-entries" | "menu" | "contact-form";
50
+ export type WidgetTypeKey = "text" | "social-links" | "recent-entries" | "menu" | "contact-form" | "gallery" | "media";
51
51
  /**
52
52
  * A widget type's declared capability class (ADR-047 Debate Fold-In Amendment
53
53
  * 3). `static` types render validated config directly with no resolver;
@@ -121,6 +121,8 @@ export interface WidgetPlacementNode {
121
121
  readonly placementId: UUID;
122
122
  readonly widgetEntryId: UUID;
123
123
  readonly enabled: boolean;
124
+ /** Optional per-placement config overlay, resolved before media lookup. */
125
+ readonly content?: JsonObject;
124
126
  }
125
127
  /** The full region-composition document stored in a `widget_area` entry's `bodyJson`. */
126
128
  export interface WidgetAreaDoc {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/cms",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -16,6 +16,7 @@
16
16
  ".": "universal",
17
17
  "./core": "universal",
18
18
  "./navigation": "universal",
19
+ "./routing": "universal",
19
20
  "./media": "node",
20
21
  "./settings": "universal",
21
22
  "./workspace": "universal",
@@ -46,7 +47,12 @@
46
47
  "./widgets/markers": "universal",
47
48
  "./widgets/html": "node",
48
49
  "./widgets/resolvers": "universal",
49
- "./widgets/sql": "node"
50
+ "./widgets/sql": "node",
51
+ "./consent": "universal",
52
+ "./consent/html": "node",
53
+ "./consent/express": "node",
54
+ "./consent/node": "node",
55
+ "./listing": "universal"
50
56
  },
51
57
  "isolation": {
52
58
  "domains": {
@@ -65,6 +71,9 @@
65
71
  "navigation": [
66
72
  "src/navigation"
67
73
  ],
74
+ "routing": [
75
+ "src/routing"
76
+ ],
68
77
  "presentation": [
69
78
  "src/presentation"
70
79
  ],
@@ -94,6 +103,12 @@
94
103
  ],
95
104
  "widgets": [
96
105
  "src/widgets"
106
+ ],
107
+ "consent": [
108
+ "src/consent"
109
+ ],
110
+ "listing": [
111
+ "src/listing"
97
112
  ]
98
113
  },
99
114
  "forbidden": {
@@ -134,6 +149,11 @@
134
149
  "import": "./dist/navigation/index.js",
135
150
  "default": "./dist/navigation/index.js"
136
151
  },
152
+ "./routing": {
153
+ "types": "./dist/routing/index.d.ts",
154
+ "import": "./dist/routing/index.js",
155
+ "default": "./dist/routing/index.js"
156
+ },
137
157
  "./media": {
138
158
  "types": "./dist/media/index.d.ts",
139
159
  "import": "./dist/media/index.js",
@@ -288,6 +308,31 @@
288
308
  "types": "./dist/widgets/sql/index.d.ts",
289
309
  "import": "./dist/widgets/sql/index.js",
290
310
  "default": "./dist/widgets/sql/index.js"
311
+ },
312
+ "./consent": {
313
+ "types": "./dist/consent/index.d.ts",
314
+ "import": "./dist/consent/index.js",
315
+ "default": "./dist/consent/index.js"
316
+ },
317
+ "./consent/html": {
318
+ "types": "./dist/consent/html/index.d.ts",
319
+ "import": "./dist/consent/html/index.js",
320
+ "default": "./dist/consent/html/index.js"
321
+ },
322
+ "./consent/express": {
323
+ "types": "./dist/consent/express/index.d.ts",
324
+ "import": "./dist/consent/express/index.js",
325
+ "default": "./dist/consent/express/index.js"
326
+ },
327
+ "./consent/node": {
328
+ "types": "./dist/consent/node/index.d.ts",
329
+ "import": "./dist/consent/node/index.js",
330
+ "default": "./dist/consent/node/index.js"
331
+ },
332
+ "./listing": {
333
+ "types": "./dist/listing/index.d.ts",
334
+ "import": "./dist/listing/index.js",
335
+ "default": "./dist/listing/index.js"
291
336
  }
292
337
  },
293
338
  "files": [
@@ -319,18 +364,18 @@
319
364
  "@types/better-sqlite3": "^7.6.13",
320
365
  "@electric-sql/pglite": "0.5.8",
321
366
  "jsdom": "^25.0.1",
322
- "@jini-ai/core": "0.4.2",
323
367
  "@jini-ai/http-kit": "0.4.1",
324
- "@jini-ai/db": "0.3.1"
368
+ "@jini-ai/db": "0.3.2",
369
+ "@jini-ai/core": "0.4.3"
325
370
  },
326
371
  "dependencies": {
327
- "@jini-ai/core": "^0.4.2"
372
+ "@jini-ai/core": "^0.4.3"
328
373
  },
329
374
  "peerDependencies": {
330
375
  "sharp": "^0.35.3",
331
376
  "express": "^4.21.0",
332
377
  "kysely": "^0.29.6",
333
- "@jini-ai/db": "^0.3.0",
378
+ "@jini-ai/db": "^0.3.2",
334
379
  "parse5": "^7.3.0",
335
380
  "@jini-ai/http-kit": "^0.4.0"
336
381
  },
@@ -1,20 +0,0 @@
1
- import { type SettingsWriteServiceDeps, type SettingsRepoPort } from "../../settings/index.js";
2
- import type { SettingsService, SettingsPermissions, SettingsChangeFeed } from "./contracts.js";
3
- /** Keep the CMS write chokepoint while translating its permission vocabulary to host policy. */
4
- /** Bind the CMS ledger and write chokepoint without copying their domain logic.
5
- * Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
6
- * @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
7
- * @example createCmsSettingsService({ deps: cmsPorts, permissions });
8
- */
9
- export declare function createCmsSettingsService(required: {
10
- deps: SettingsWriteServiceDeps;
11
- permissions: SettingsPermissions;
12
- }): SettingsService;
13
- /** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
14
- * @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
15
- * @example createCmsSettingsChangeFeed({ repo });
16
- */
17
- export declare function createCmsSettingsChangeFeed(required: {
18
- repo: SettingsRepoPort;
19
- }): SettingsChangeFeed;
20
- //# sourceMappingURL=cms-adapter.d.ts.map
@@ -1,124 +0,0 @@
1
- import { getEffective, resolveDefinition, set, clear, resetNamespace, registerDefinitions, NON_REGISTER_DEFINITION_OPS, parseNonRegisterDefinitionOp, collectChangedNamespaces, } from "../../settings/index.js";
2
- /** Keep the CMS write chokepoint while translating its permission vocabulary to host policy. */
3
- /** Bind the CMS ledger and write chokepoint without copying their domain logic.
4
- * Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
5
- * @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
6
- * @example createCmsSettingsService({ deps: cmsPorts, permissions });
7
- */
8
- export function createCmsSettingsService(required) {
9
- const { deps: original, permissions } = required;
10
- const permissionMap = new Map([
11
- ["settings.definitions.manage", permissions.manageDefinitions],
12
- ["settings.global.write", permissions.writeGlobal],
13
- ["settings.workspace.write", permissions.writeWorkspace],
14
- ["settings.user.self.write", permissions.writeUserSelf],
15
- ["settings.user.write", permissions.writeUserOther],
16
- ["settings.reset.global", permissions.reset.global],
17
- ["settings.reset.workspace", permissions.reset.workspace],
18
- ["settings.reset.user", permissions.reset.user],
19
- ]);
20
- const deps = { ...original,
21
- authorize: (input) => {
22
- const permission = permissionMap.get(input.permission);
23
- if (permission === undefined)
24
- return Promise.resolve({ allowed: false, reason: "unknown_settings_permission" });
25
- return original.authorize({ ...input, permission });
26
- },
27
- };
28
- const repo = deps.repo;
29
- const listDefinitions = async ({ workspaceId }) => {
30
- const [platform, workspace] = await Promise.all([
31
- repo.listActiveDefinitions({ workspaceId: null }), repo.listActiveDefinitions({ workspaceId }),
32
- ]);
33
- return [...platform, ...workspace];
34
- };
35
- return {
36
- listDefinitions,
37
- effective: async ({ namespace, workspaceId, principalId }) => {
38
- const defs = await listDefinitions({ workspaceId });
39
- const keys = new Set(defs.filter((d) => d.namespace === namespace).map((d) => d.key));
40
- const data = [];
41
- for (const key of keys) {
42
- const result = await getEffective({ repo }, { namespace, key, scopeContext: { workspaceId, principalId } });
43
- if (result)
44
- data.push({ key, value: result.value, sourceLayer: result.sourceLayer, defVersion: result.defVersion });
45
- }
46
- return data;
47
- },
48
- raw: async ({ namespace, key, workspaceId, principalId }) => {
49
- const definition = await resolveDefinition({ repo }, { namespace, key, workspaceId });
50
- if (!definition)
51
- return null;
52
- const settingId = definition.settingId;
53
- const records = await Promise.all([
54
- repo.getGlobalValue(settingId), repo.getWorkspaceValue({ workspaceId, settingId }),
55
- principalId ? repo.getUserValue({ workspaceId, principalId, settingId }) : Promise.resolve(null),
56
- ]);
57
- const values = records.map((r) => r?.state === "set" ? r.valueJson : null);
58
- return { key: `${namespace}.${key}`, global: values[0], workspace: values[1], user: values[2], default: definition.defaultValue };
59
- },
60
- set: (input) => set({ deps, input }),
61
- clear: (input) => clear({ deps, input }),
62
- reset: async (input) => {
63
- const definitions = await repo.listActiveDefinitions({ workspaceId: input.scope === "global" ? null : (input.workspaceId ?? input.authWorkspaceId ?? null) });
64
- const keys = definitions.filter((d) => d.namespace === input.namespace).map((d) => d.key);
65
- return resetNamespace({ deps, input }, keys);
66
- },
67
- definitions: ({ items, callerPrincipalId, authWorkspaceId }) => applyDefinitions({ deps, items, callerPrincipalId, authWorkspaceId }),
68
- };
69
- }
70
- /** Preserve register batching and ordered non-register writes over existing CMS operation contracts.
71
- * Unknown operations return their name. Earlier non-register writes are not rolled back by later failures.
72
- * @complexity O(item count) time/space plus CMS effects; this batch is not atomic.
73
- */
74
- async function applyDefinitions(required) {
75
- const { deps, items, callerPrincipalId, authWorkspaceId } = required;
76
- const toRegister = [];
77
- const applied = [];
78
- for (const raw of items) {
79
- const item = raw;
80
- const op = String(item.op ?? "register");
81
- const namespace = String(item.namespace ?? "");
82
- const key = String(item.key ?? "");
83
- const ownerKind = item.ownerKind;
84
- const workspaceId = ownerKind === "site" ? authWorkspaceId : null;
85
- const defaultValue = (item.defaultJson ?? null);
86
- if (op === "register") {
87
- toRegister.push({ namespace, key, ownerKind, workspaceId, schema: item.schemaJson,
88
- defaultValue, scopes: Number(item.scopes), secret: Boolean(item.secret ?? false) });
89
- }
90
- else {
91
- const parsedOp = parseNonRegisterDefinitionOp({ op });
92
- if (!parsedOp)
93
- return { unknownOp: op };
94
- const opItem = { namespace, key, ownerKind, workspaceId, defaultJson: defaultValue,
95
- ...(item.newNamespace === undefined ? {} : { newNamespace: item.newNamespace }),
96
- ...(item.newKey === undefined ? {} : { newKey: item.newKey }),
97
- ...(item.schemaJson === undefined ? {} : { schemaJson: item.schemaJson }),
98
- ...(item.coercionJson === undefined ? {} : { coercionJson: item.coercionJson }),
99
- };
100
- await NON_REGISTER_DEFINITION_OPS[parsedOp]({ ctx: { deps, callerPrincipalId, authWorkspaceId }, item: opItem });
101
- }
102
- applied.push({ key: `${namespace}.${key}`, op, status: "applied" });
103
- }
104
- if (toRegister.length)
105
- await registerDefinitions({ deps, input: { definitions: toRegister, callerPrincipalId, authWorkspaceId } });
106
- return { applied };
107
- }
108
- /** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
109
- * @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
110
- * @example createCmsSettingsChangeFeed({ repo });
111
- */
112
- export function createCmsSettingsChangeFeed(required) {
113
- const { repo } = required;
114
- return {
115
- head: () => repo.maxRevisionSeq(),
116
- collect: async ({ sinceSeq, limit, viewer }) => {
117
- const revisions = await repo.listRevisionsSince({ sinceSeq, limit, workspaceId: viewer.workspaceId });
118
- // Resolve afresh each batch: a rename must never leave a stale namespace cached forever.
119
- const batch = await collectChangedNamespaces({ revisions, viewer, resolveNamespace: async ({ settingId }) => (await repo.findDefinitionBySettingId({ settingId }))?.namespace ?? null });
120
- return { ...batch, examinedCount: revisions.length };
121
- },
122
- };
123
- }
124
- //# sourceMappingURL=cms-adapter.js.map