@jini-ai/cms 0.5.7 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.5.8 — 2026-10-13
2
+
3
+ - New `./navigation/node` subpath: re-exports the full `./navigation` API with parse5 menu-HTML normalization bound into `createMenu`, `updateMenuTree`, `mergeMenuHtml` and `buildMenusRegistrations`. Node hosts writing HTML-mode menus should import those from `./navigation/node`.
4
+ - `./navigation` no longer loads the optional peer `parse5`, so it is universal again. Every name is still exported; an HTML-mode write through it needs an injected `normalizeMenuHtml` (`normalizeHtml` for `mergeMenuHtml`) and otherwise fails before saving. Item-only writes are unchanged.
5
+ - `./seo` no longer reaches the media transform module for rendition MIME types; `TransformFormat` and `mimeForTransformFormat` remain exported from the media entries.
6
+
1
7
  ## 0.5.7 — 2026-10-12
2
8
 
3
9
  - New render-only leaf subpaths: `./forms/html-render`, `./forms/submission-attempts`, `./widgets/page-embed-types`, `./widgets/html/html-embeds`, `./widgets/html/gallery-render`, `./navigation/types`, `./navigation/dropdown` and `./media/slug` (the media slug rule, still re-exported from `./media`). Existing barrels are unchanged.
package/README.md CHANGED
@@ -78,6 +78,13 @@ coordination.
78
78
  `taxonomy`, `workspace`, and `trash`. `settings` is now a compatibility shim, with implementation
79
79
  in `@jini-ai/core/settings`. `./core/tools` re-exports the shared core permission helpers; `./media/import` exports bounded image fetching with injected policies and ports.
80
80
 
81
+ `./navigation` stays universal: submitted menu HTML requires an injected `normalizeMenuHtml`
82
+ dependency (`normalizeHtml` for `mergeMenuHtml`). Item-only writes and retained markup need no
83
+ parser. Node hosts use `./navigation/node`, which re-exports the navigation API and supplies
84
+ parse5 normalization to create/update, merge and tool registration. The existing validation,
85
+ raw-HTML permission checks, OCC and outbox owners remain unchanged; an unconfigured universal
86
+ HTML write fails before saving rather than persisting markup that could escape its theme container.
87
+
81
88
  **Still to move:** `post`, `seo`, `comments`, `redirects`, `widgets`, `newsletter`,
82
89
  `members`. The unimplemented `./widgets` export has been removed.
83
90
  The nested forms package is maintained by its separate owner and is excluded from this parent package.
@@ -0,0 +1,19 @@
1
+ /** Shared rendition format metadata. Media validation and SEO use this leaf without loading
2
+ * either domain's runtime; media/transform-types.ts preserves its existing public exports.
3
+ */
4
+ /**
5
+ * Output re-encode format. Matches `media-service.ts`'s
6
+ * `DEFAULT_ALLOWED_MIME_TYPES` image set minus SVG (never a transform output —
7
+ * SVG has no raster pixels to resize).
8
+ */
9
+ export type TransformFormat = "jpeg" | "png" | "webp" | "gif";
10
+ /** Maps a {@link TransformFormat} to the MIME type served on the rendition response. */
11
+ export declare const MIME_BY_TRANSFORM_FORMAT: Record<TransformFormat, string>;
12
+ /**
13
+ * @complexity O(1).
14
+ * @overallScore 100
15
+ */
16
+ export declare function mimeForTransformFormat(requiredArgs: {
17
+ format: TransformFormat;
18
+ }, optionalArgs?: Record<string, never>): string;
19
+ //# sourceMappingURL=transform-format.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Shared rendition format metadata. Media validation and SEO use this leaf without loading
2
+ * either domain's runtime; media/transform-types.ts preserves its existing public exports.
3
+ */
4
+ /** Maps a {@link TransformFormat} to the MIME type served on the rendition response. */
5
+ export const MIME_BY_TRANSFORM_FORMAT = {
6
+ jpeg: "image/jpeg",
7
+ png: "image/png",
8
+ webp: "image/webp",
9
+ gif: "image/gif",
10
+ };
11
+ /**
12
+ * @complexity O(1).
13
+ * @overallScore 100
14
+ */
15
+ export function mimeForTransformFormat(requiredArgs, optionalArgs = {}) {
16
+ const { format } = requiredArgs;
17
+ return MIME_BY_TRANSFORM_FORMAT[format];
18
+ }
19
+ //# sourceMappingURL=transform-format.js.map
@@ -22,14 +22,10 @@
22
22
  * permission catalog.
23
23
  */
24
24
  import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
25
+ import { type TransformFormat } from "../core/transform-format.js";
26
+ export { mimeForTransformFormat, type TransformFormat } from "../core/transform-format.js";
25
27
  /** Resize behavior, mirrors `sharp`'s `fit` vocabulary (a deliberately small subset). */
26
28
  export type TransformFit = "cover" | "contain" | "fill" | "inside" | "outside";
27
- /**
28
- * Output re-encode format. Matches `media-service.ts`'s
29
- * `DEFAULT_ALLOWED_MIME_TYPES` image set minus SVG (never a transform output —
30
- * SVG has no raster pixels to resize).
31
- */
32
- export type TransformFormat = "jpeg" | "png" | "webp" | "gif";
33
29
  /**
34
30
  * Declarative transform parameters (named definitions only, never
35
31
  * caller-supplied dimensions). `width`/`height` are optional so a
@@ -66,13 +62,6 @@ export declare class TransformValidationError extends Error {
66
62
  message: string;
67
63
  }, optionalArgs?: ErrorOptions);
68
64
  }
69
- /**
70
- * @complexity O(1).
71
- * @overallScore 100
72
- */
73
- export declare function mimeForTransformFormat(requiredArgs: {
74
- format: TransformFormat;
75
- }, optionalArgs?: Record<string, never>): string;
76
65
  /** Upper bound on a requested resize dimension — an output-side resource-use guard (not the
77
66
  * full pixel-bomb ingest cap, which polices upload input; this polices the transform
78
67
  * definition itself so a registered definition can't ask the image transformer to allocate an
@@ -1,23 +1,10 @@
1
+ import { MIME_BY_TRANSFORM_FORMAT } from "../core/transform-format.js";
2
+ export { mimeForTransformFormat } from "../core/transform-format.js";
1
3
  export class TransformValidationError extends Error {
2
4
  constructor({ message }, optionalArgs = {}) {
3
5
  super(message, optionalArgs);
4
6
  }
5
7
  }
6
- /** Maps a {@link TransformFormat} to the MIME type served on the rendition response. */
7
- const MIME_BY_TRANSFORM_FORMAT = {
8
- jpeg: "image/jpeg",
9
- png: "image/png",
10
- webp: "image/webp",
11
- gif: "image/gif",
12
- };
13
- /**
14
- * @complexity O(1).
15
- * @overallScore 100
16
- */
17
- export function mimeForTransformFormat(requiredArgs, optionalArgs = {}) {
18
- const { format } = requiredArgs;
19
- return MIME_BY_TRANSFORM_FORMAT[format];
20
- }
21
8
  /** Upper bound on a requested resize dimension — an output-side resource-use guard (not the
22
9
  * full pixel-bomb ingest cap, which polices upload input; this polices the transform
23
10
  * definition itself so a registered definition can't ask the image transformer to allocate an
@@ -27,7 +27,7 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, menuVersionConflictEr
27
27
  */
28
28
  export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuVersionConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, type TreeValidationLimits, type CreateMenuDeps, type CreateMenuServiceInput, type CreateMenuRequired, type CreateMenuOptional, type UpdateMenuTreeDeps, type UpdateMenuTreeServiceInput, type UpdateMenuTreeRequired, type UpdateMenuTreeOptional, type AssignLocationDeps, type AssignLocationServiceInput, type AssignLocationRequired, type AssignLocationOptional, type DeleteMenuDeps, type DeleteMenuServiceInput, type DeleteMenuRequired, type DeleteMenuOptional, } from "./menu-service.js";
29
29
  /** HTML mode (2026-10-08): validation/normalization for a menu's author-written markup. */
30
- export { MAX_MENU_HTML_LENGTH, isMenuHtmlAuthoring, menuHtmlInputProblem, mergeMenuHtml, type MenuHtmlAuthoring, } from "./menu-html.js";
30
+ export { MAX_MENU_HTML_LENGTH, isMenuHtmlAuthoring, menuHtmlInputProblem, mergeMenuHtml, type MenuHtmlAuthoring, type MenuHtmlNormalizer, type MenuHtmlOptions, } from "./menu-html.js";
31
31
  export { resolveForLocation, resolveMenuDoc, type ResolveTargetHrefFn, type ResolvedTargetHref, type ResolveForLocationDeps, type ResolveForLocationServiceInput, type ResolveForLocationRequired, type ResolveForLocationOptional, } from "./resolver.js";
32
32
  export { rebuildNavLocationBindings, type RebuildNavLocationBindingsDeps, type RebuildNavLocationBindingsResult, } from "./reconcile.js";
33
33
  /** Dropdown menus (2026-10-11): toggle markup + the inline keyboard/touch script (see `dropdown.ts`). */
@@ -1,3 +1,17 @@
1
+ /**
2
+ * @file HTML mode for a menu (2026-10-08): the author writes the menu's markup by hand instead of
3
+ * building an item tree, so it can be styled however they want.
4
+ *
5
+ * Same trust model as HTML-mode forms (`forms/html/html-authoring.ts`) and HTML Pages: the markup is
6
+ * admin/owner-trusted and NOT sanitized. Hosts gate it behind their raw-HTML permission (Tovu binds
7
+ * `pages.edit_html`, the one forms use); this module only validates and merges. The injected
8
+ * normalizer balances the fragment so a stray `</nav>` or an unclosed `<div>` cannot close or
9
+ * swallow the theme element the menu renders inside. The Node adapter supplies parse5; the
10
+ * universal entry never loads a parser or assumes a browser DOM.
11
+ *
12
+ * Both modes' data live side by side in the doc: switching mode never deletes the other one, so an
13
+ * author can flip back and find their items (or their HTML) still there.
14
+ */
1
15
  import type { NavMenuDoc, NavMenuMode } from "./types.js";
2
16
  /** Same ceiling HTML-mode forms use. */
3
17
  export declare const MAX_MENU_HTML_LENGTH = 200000;
@@ -12,6 +26,14 @@ export interface MenuHtmlAuthoring {
12
26
  * @complexity O(1).
13
27
  */
14
28
  export declare function isMenuHtmlAuthoring({ mode, html }: MenuHtmlAuthoring, _optional?: Record<string, never>): boolean;
29
+ /** Balance a trusted HTML fragment while preserving scripts and authored styling. */
30
+ export type MenuHtmlNormalizer = (required: {
31
+ html: string;
32
+ }, optional?: Record<string, never>) => string;
33
+ /** A host supplies its environment's fragment parser; omitted is valid for item-only writes. */
34
+ export interface MenuHtmlOptions {
35
+ normalizeHtml?: MenuHtmlNormalizer | undefined;
36
+ }
15
37
  /**
16
38
  * Why a write's `mode`/`html` is invalid, or `null` when it is fine. A message rather than a throw so
17
39
  * `menu-service.ts` raises its own `MenuValidationError` without this file importing it back.
@@ -22,10 +44,12 @@ export declare function menuHtmlInputProblem({ mode, html }: MenuHtmlAuthoring,
22
44
  * The doc's `mode`/`html` after a valid write: submitted values win, omitted ones keep what is
23
45
  * stored, and a menu that never used HTML mode gets neither key, so its stored doc (and publish
24
46
  * hash) is unchanged. Call {@link menuHtmlInputProblem} first.
47
+ * @param optional Host fragment normalizer, required when input.html is supplied.
48
+ * @throws {Error} Submitted markup has no configured normalizer; never saves unbalanced HTML.
25
49
  * @complexity O(h) in the submitted html length (one parse + serialize).
26
50
  */
27
51
  export declare function mergeMenuHtml({ input, existing }: {
28
52
  input: MenuHtmlAuthoring;
29
53
  existing?: Pick<NavMenuDoc, "mode" | "html"> | undefined;
30
- }, _optional?: Record<string, never>): Pick<NavMenuDoc, "mode" | "html">;
54
+ }, optional?: MenuHtmlOptions): Pick<NavMenuDoc, "mode" | "html">;
31
55
  //# sourceMappingURL=menu-html.d.ts.map
@@ -1,17 +1,3 @@
1
- /**
2
- * @file HTML mode for a menu (2026-10-08): the author writes the menu's markup by hand instead of
3
- * building an item tree, so it can be styled however they want.
4
- *
5
- * Same trust model as HTML-mode forms (`forms/html/html-authoring.ts`) and HTML Pages: the markup is
6
- * admin/owner-trusted and NOT sanitized. Hosts gate it behind their raw-HTML permission (Tovu binds
7
- * `pages.edit_html`, the one forms use); this module only validates and normalizes. parse5 balances
8
- * the fragment so a stray `</nav>` or an unclosed `<div>` cannot close or swallow the theme element
9
- * the menu renders inside.
10
- *
11
- * Both modes' data live side by side in the doc: switching mode never deletes the other one, so an
12
- * author can flip back and find their items (or their HTML) still there.
13
- */
14
- import { parseFragment, serialize } from "parse5";
15
1
  /** Same ceiling HTML-mode forms use. */
16
2
  export const MAX_MENU_HTML_LENGTH = 200_000;
17
3
  /**
@@ -22,10 +8,6 @@ export const MAX_MENU_HTML_LENGTH = 200_000;
22
8
  export function isMenuHtmlAuthoring({ mode, html }, _optional = {}) {
23
9
  return html !== undefined || mode === "html";
24
10
  }
25
- /** Balance the fragment; scripts and authored styling stay as written (trusted markup). */
26
- function normalizeMenuHtml(html) {
27
- return serialize(parseFragment(html));
28
- }
29
11
  /**
30
12
  * Why a write's `mode`/`html` is invalid, or `null` when it is fine. A message rather than a throw so
31
13
  * `menu-service.ts` raises its own `MenuValidationError` without this file importing it back.
@@ -43,11 +25,18 @@ export function menuHtmlInputProblem({ mode, html }, _optional = {}) {
43
25
  * The doc's `mode`/`html` after a valid write: submitted values win, omitted ones keep what is
44
26
  * stored, and a menu that never used HTML mode gets neither key, so its stored doc (and publish
45
27
  * hash) is unchanged. Call {@link menuHtmlInputProblem} first.
28
+ * @param optional Host fragment normalizer, required when input.html is supplied.
29
+ * @throws {Error} Submitted markup has no configured normalizer; never saves unbalanced HTML.
46
30
  * @complexity O(h) in the submitted html length (one parse + serialize).
47
31
  */
48
- export function mergeMenuHtml({ input, existing }, _optional = {}) {
32
+ export function mergeMenuHtml({ input, existing }, optional = {}) {
49
33
  const mode = input.mode ?? existing?.mode;
50
- const html = input.html !== undefined ? normalizeMenuHtml(input.html) : existing?.html;
34
+ let html = existing?.html;
35
+ if (input.html !== undefined) {
36
+ if (!optional.normalizeHtml)
37
+ throw new Error("Menu HTML normalization is not configured");
38
+ html = optional.normalizeHtml({ html: input.html });
39
+ }
51
40
  return { ...(mode !== undefined ? { mode } : {}), ...(html !== undefined ? { html } : {}) };
52
41
  }
53
42
  //# sourceMappingURL=menu-html.js.map
@@ -2,6 +2,7 @@ import type { Clock, IdGenerator, UUID } from "@jini-ai/core/primitives";
2
2
  import type { OutboxPort } from "../core/ports.js";
3
3
  import type { MenuRepoPort } from "./repo.memory.js";
4
4
  import type { NavLocationBindingRepoPort } from "./ports.js";
5
+ import { type MenuHtmlNormalizer } from "./menu-html.js";
5
6
  import { type NavItemNode, type NavLocationBindingRow, type NavLocationKey, type NavMenuEntry, type NavMenuMode } from "./types.js";
6
7
  export declare class MenuNotFoundError extends Error {
7
8
  constructor({ message }: {
@@ -135,6 +136,8 @@ export declare function validateAndCloneTree({ items }: {
135
136
  items: readonly NavItemNode[];
136
137
  }, limits?: TreeValidationLimits): NavItemNode[];
137
138
  export interface CreateMenuDeps {
139
+ /** Required for submitted HTML; Node hosts can use the navigation/node adapter. */
140
+ normalizeMenuHtml?: MenuHtmlNormalizer | undefined;
138
141
  repo: MenuRepoPort;
139
142
  clock: Clock;
140
143
  idGen: IdGenerator;
@@ -177,6 +180,8 @@ export declare function createMenu(required: CreateMenuRequired, optional?: Crea
177
180
  menu: NavMenuEntry;
178
181
  }>;
179
182
  export interface UpdateMenuTreeDeps {
183
+ /** Required for submitted HTML; omitted markup retains the already-normalized stored value. */
184
+ normalizeMenuHtml?: MenuHtmlNormalizer | undefined;
180
185
  repo: MenuRepoPort;
181
186
  clock: Clock;
182
187
  /** Not previously present on this deps bag — needed to mint the outbox event's id. */
@@ -283,11 +283,11 @@ function assertValidTitleAndSlug(title, slug) {
283
283
  }
284
284
  }
285
285
  /** Validated, merged HTML-mode doc fields — `menu-html.ts`'s problem check raised as this file's own error. */
286
- function menuHtmlFields({ input, existing }) {
286
+ function menuHtmlFields({ input, existing, normalizeHtml }) {
287
287
  const problem = menuHtmlInputProblem(input);
288
288
  if (problem)
289
289
  throw new MenuValidationError({ message: problem });
290
- return mergeMenuHtml({ input, existing });
290
+ return mergeMenuHtml({ input, existing }, { normalizeHtml });
291
291
  }
292
292
  /**
293
293
  * Creates a new `menu` entry in `published` status with no location
@@ -313,7 +313,7 @@ export async function createMenu(required, optional = {}) {
313
313
  if (duplicate)
314
314
  throw new MenuConflictError({ message: `slug '${slug}' already exists` });
315
315
  const items = validateAndCloneTree({ items: input.items ?? [] }, optional.limits);
316
- const htmlFields = menuHtmlFields({ input });
316
+ const htmlFields = menuHtmlFields({ input, normalizeHtml: deps.normalizeMenuHtml });
317
317
  const menu = {
318
318
  id: deps.idGen.newId(),
319
319
  workspaceId: input.workspaceId,
@@ -422,7 +422,7 @@ export async function updateMenuTree(required, optional = {}) {
422
422
  });
423
423
  // Every writer (admin, agent tool, publish repoint, reverters) comes through here, so the stored
424
424
  // HTML-mode fields are carried forward unless this write sets them — an items-only save never drops them.
425
- const htmlFields = menuHtmlFields({ input, existing: existing.doc });
425
+ const htmlFields = menuHtmlFields({ input, existing: existing.doc, normalizeHtml: deps.normalizeMenuHtml });
426
426
  const menu = {
427
427
  ...existing,
428
428
  title,
@@ -0,0 +1,30 @@
1
+ import { mergeMenuHtml as mergeUniversalMenuHtml, type MenuHtmlOptions } from "../menu-html.js";
2
+ import { createMenu as createUniversalMenu, updateMenuTree as updateUniversalMenuTree, type CreateMenuRequired, type CreateMenuOptional, type UpdateMenuTreeRequired, type UpdateMenuTreeOptional } from "../menu-service.js";
3
+ import { buildMenusRegistrations as buildUniversalMenusRegistrations, type MenusToolDeps } from "../tool-registrations.js";
4
+ export * from "../index.js";
5
+ /** Balance the fragment; scripts and authored styling stay as written (trusted markup).
6
+ * @param required Trusted authored markup; the service enforces the length cap before calling.
7
+ * @returns Balanced fragment, without dangling outer closers.
8
+ * @complexity O(h) time and space in submitted HTML length.
9
+ */
10
+ export declare function normalizeMenuHtml({ html }: {
11
+ html: string;
12
+ }, _optional?: Record<string, never>): string;
13
+ /** Supply Node normalization to the universal create owner; validation, errors, writes and
14
+ * outbox publication are unchanged. Caller-provided normalization takes precedence.
15
+ * @complexity Same tree/slug scans as the universal owner, plus O(h) HTML time and space.
16
+ */
17
+ export declare function createMenu(required: CreateMenuRequired, optional?: CreateMenuOptional): ReturnType<typeof createUniversalMenu>;
18
+ /** Supply Node normalization to the universal update owner, retaining its OCC and event contract.
19
+ * @complexity Same tree/slug scans as the universal owner, plus O(h) HTML time and space.
20
+ */
21
+ export declare function updateMenuTree(required: UpdateMenuTreeRequired, optional?: UpdateMenuTreeOptional): ReturnType<typeof updateUniversalMenuTree>;
22
+ /** Preserve the merge API for Node consumers, balancing only submitted HTML.
23
+ * @complexity O(h) time and space in submitted HTML length; omitted HTML is O(1).
24
+ */
25
+ export declare function mergeMenuHtml(required: Parameters<typeof mergeUniversalMenuHtml>[0], optional?: MenuHtmlOptions): ReturnType<typeof mergeUniversalMenuHtml>;
26
+ /** Bind Node normalization once for the existing tool builder; permission checks and saves
27
+ * remain in their universal owners. Construction is O(1) time and space (fixed catalog).
28
+ */
29
+ export declare function buildMenusRegistrations(required: MenusToolDeps, _optional?: Record<string, never>): ReturnType<typeof buildUniversalMenusRegistrations>;
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,43 @@
1
+ /** Node navigation adapter: the public barrel stays compatible, while HTML writes receive
2
+ * parse5 normalization. Item services and all persistence/permission policy stay universal.
3
+ * @example import { createMenu } from "@jini-ai/cms/navigation/node";
4
+ */
5
+ import { parseFragment, serialize } from "parse5";
6
+ import { mergeMenuHtml as mergeUniversalMenuHtml } from "../menu-html.js";
7
+ import { createMenu as createUniversalMenu, updateMenuTree as updateUniversalMenuTree, } from "../menu-service.js";
8
+ import { buildMenusRegistrations as buildUniversalMenusRegistrations } from "../tool-registrations.js";
9
+ export * from "../index.js";
10
+ /** Balance the fragment; scripts and authored styling stay as written (trusted markup).
11
+ * @param required Trusted authored markup; the service enforces the length cap before calling.
12
+ * @returns Balanced fragment, without dangling outer closers.
13
+ * @complexity O(h) time and space in submitted HTML length.
14
+ */
15
+ export function normalizeMenuHtml({ html }, _optional = {}) {
16
+ return serialize(parseFragment(html));
17
+ }
18
+ /** Supply Node normalization to the universal create owner; validation, errors, writes and
19
+ * outbox publication are unchanged. Caller-provided normalization takes precedence.
20
+ * @complexity Same tree/slug scans as the universal owner, plus O(h) HTML time and space.
21
+ */
22
+ export function createMenu(required, optional = {}) {
23
+ return createUniversalMenu({ ...required, deps: { ...required.deps, normalizeMenuHtml: required.deps.normalizeMenuHtml ?? normalizeMenuHtml } }, optional);
24
+ }
25
+ /** Supply Node normalization to the universal update owner, retaining its OCC and event contract.
26
+ * @complexity Same tree/slug scans as the universal owner, plus O(h) HTML time and space.
27
+ */
28
+ export function updateMenuTree(required, optional = {}) {
29
+ return updateUniversalMenuTree({ ...required, deps: { ...required.deps, normalizeMenuHtml: required.deps.normalizeMenuHtml ?? normalizeMenuHtml } }, optional);
30
+ }
31
+ /** Preserve the merge API for Node consumers, balancing only submitted HTML.
32
+ * @complexity O(h) time and space in submitted HTML length; omitted HTML is O(1).
33
+ */
34
+ export function mergeMenuHtml(required, optional = {}) {
35
+ return mergeUniversalMenuHtml(required, { ...optional, normalizeHtml: optional.normalizeHtml ?? normalizeMenuHtml });
36
+ }
37
+ /** Bind Node normalization once for the existing tool builder; permission checks and saves
38
+ * remain in their universal owners. Construction is O(1) time and space (fixed catalog).
39
+ */
40
+ export function buildMenusRegistrations(required, _optional = {}) {
41
+ return buildUniversalMenusRegistrations({ ...required, normalizeMenuHtml: required.normalizeMenuHtml ?? normalizeMenuHtml });
42
+ }
43
+ //# sourceMappingURL=index.js.map
@@ -3,6 +3,7 @@ import type { Clock } from "@jini-ai/core/primitives";
3
3
  import type { AuthorizeFn } from "../core/commands/command.js";
4
4
  import type { OutboxPort } from "../core/ports.js";
5
5
  import { type DerivedRiskByToolId, type ToolRegistration } from "@jini-ai/core";
6
+ import { type MenuHtmlNormalizer } from "./menu-html.js";
6
7
  import type { NavLocationBindingRepoPort } from "./ports.js";
7
8
  import type { MenuRepoPort } from "./repo.memory.js";
8
9
  /**
@@ -12,6 +13,8 @@ import type { MenuRepoPort } from "./repo.memory.js";
12
13
  * route-deps object after binding the kernel clock/ID contracts.
13
14
  */
14
15
  export interface MenusToolDeps {
16
+ /** Environment-owned fragment normalizer, forwarded to the audited menu write services. */
17
+ normalizeMenuHtml?: MenuHtmlNormalizer | undefined;
15
18
  authorize: AuthorizeFn;
16
19
  workspaceId: string;
17
20
  clock: Clock;
@@ -80,7 +80,7 @@ async function requireHtmlAuthoringPermission({ routeDeps, principalId, authorin
80
80
  await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId, permission: routeDeps.rawHtmlPermission }, { entityType: "menu", ...(menuId ? { entityId: menuId } : {}) });
81
81
  }
82
82
  function menusDeps(routeDeps) {
83
- return { repo: routeDeps.menuRepo, clock: routeDeps.clock, idGen: routeDeps.idGen, outbox: routeDeps.outbox };
83
+ return { repo: routeDeps.menuRepo, clock: routeDeps.clock, idGen: routeDeps.idGen, outbox: routeDeps.outbox, normalizeMenuHtml: routeDeps.normalizeMenuHtml };
84
84
  }
85
85
  export function buildMenusRegistrations(routeDeps) {
86
86
  const handlers = {
package/dist/seo/media.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mimeForTransformFormat } from "../media/transform-types.js";
1
+ import { mimeForTransformFormat } from "../core/transform-format.js";
2
2
  /**
3
3
  * @file `resolveSeoImageRef` (ADR-PIPE-008 Decision §6, C-013, EC-07;
4
4
  * EC-07's "never generates" clause AMENDED 2026-09-05 — see the ADR's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/cms",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
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": {
@@ -36,6 +36,7 @@
36
36
  "./widgets/html/gallery-render": "universal",
37
37
  "./navigation/types": "universal",
38
38
  "./navigation/dropdown": "universal",
39
+ "./navigation/node": "node",
39
40
  "./media/slug": "universal",
40
41
  "./forms": "universal",
41
42
  "./forms/html": "node",
@@ -257,6 +258,11 @@
257
258
  "import": "./dist/navigation/dropdown.js",
258
259
  "default": "./dist/navigation/dropdown.js"
259
260
  },
261
+ "./navigation/node": {
262
+ "types": "./dist/navigation/node/index.d.ts",
263
+ "import": "./dist/navigation/node/index.js",
264
+ "default": "./dist/navigation/node/index.js"
265
+ },
260
266
  "./media/slug": {
261
267
  "types": "./dist/media/slug.d.ts",
262
268
  "import": "./dist/media/slug.js",
@@ -413,8 +419,8 @@
413
419
  "@electric-sql/pglite": "0.5.8",
414
420
  "jsdom": "^25.0.1",
415
421
  "@jini-ai/core": "0.4.4",
416
- "@jini-ai/db": "0.3.3",
417
- "@jini-ai/http-kit": "0.4.1"
422
+ "@jini-ai/http-kit": "0.4.1",
423
+ "@jini-ai/db": "0.3.3"
418
424
  },
419
425
  "dependencies": {
420
426
  "@jini-ai/core": "^0.4.4"