@jini-ai/cms 0.5.7 → 0.5.9
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 +10 -0
- package/README.md +7 -0
- package/dist/core/transform-format.d.ts +19 -0
- package/dist/core/transform-format.js +19 -0
- package/dist/media/transform-types.d.ts +2 -13
- package/dist/media/transform-types.js +2 -15
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/menu-html.d.ts +25 -1
- package/dist/navigation/menu-html.js +9 -20
- package/dist/navigation/menu-service.d.ts +5 -0
- package/dist/navigation/menu-service.js +4 -4
- package/dist/navigation/node/index.d.ts +30 -0
- package/dist/navigation/node/index.js +43 -0
- package/dist/navigation/tool-registrations.d.ts +3 -0
- package/dist/navigation/tool-registrations.js +1 -1
- package/dist/seo/media.js +1 -1
- package/dist/widgets/concurrency.d.ts +5 -0
- package/dist/widgets/concurrency.js +11 -0
- package/dist/widgets/embed-service.d.ts +3 -0
- package/dist/widgets/embed-service.js +3 -3
- package/dist/widgets/region-area-service.d.ts +3 -0
- package/dist/widgets/region-area-service.js +2 -2
- package/dist/widgets/write-service.d.ts +4 -1
- package/dist/widgets/write-service.js +3 -3
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 0.5.9 — 2026-10-13
|
|
2
|
+
|
|
3
|
+
- `./widgets`: write, embed and region services take an optional `entryLocks` dependency, threaded into every `withEntryLock` call. New `getEntryLockState({ owner })` returns one lock state per store object, so a host that rebuilds dependency bags per request still shares one queue per store. Omitting `entryLocks` keeps the previous module-wide state.
|
|
4
|
+
|
|
5
|
+
## 0.5.8 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
- 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`.
|
|
8
|
+
- `./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.
|
|
9
|
+
- `./seo` no longer reaches the media transform module for rendition MIME types; `TransformFormat` and `mimeForTransformFormat` remain exported from the media entries.
|
|
10
|
+
|
|
1
11
|
## 0.5.7 — 2026-10-12
|
|
2
12
|
|
|
3
13
|
- 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
|
-
},
|
|
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 },
|
|
32
|
+
export function mergeMenuHtml({ input, existing }, optional = {}) {
|
|
49
33
|
const mode = input.mode ?? existing?.mode;
|
|
50
|
-
|
|
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 "../
|
|
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
|
|
@@ -22,6 +22,11 @@ export type EntryLockState = Map<string, Promise<void>>;
|
|
|
22
22
|
/** A host can own this state; the compatibility adapter still serializes existing callers.
|
|
23
23
|
* Idle keys are removed by withEntryLock. @complexity O(1). */
|
|
24
24
|
export declare function createEntryLockState(_required: Record<string, never>, _optional?: Record<string, never>): EntryLockState;
|
|
25
|
+
/** Repeated dependency composition for the same store retains one lock queue.
|
|
26
|
+
* The weak owner reference lets disposed stores and their idle state be collected. @complexity O(1). */
|
|
27
|
+
export declare function getEntryLockState({ owner }: {
|
|
28
|
+
readonly owner: object;
|
|
29
|
+
}, _optional?: Record<string, never>): EntryLockState;
|
|
25
30
|
/**
|
|
26
31
|
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
27
32
|
* callers queue in call order, each waiting for the previous holder to finish.
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
export function createEntryLockState(_required, _optional = {}) {
|
|
4
4
|
return new Map();
|
|
5
5
|
}
|
|
6
|
+
const ownedLocks = new WeakMap();
|
|
7
|
+
/** Repeated dependency composition for the same store retains one lock queue.
|
|
8
|
+
* The weak owner reference lets disposed stores and their idle state be collected. @complexity O(1). */
|
|
9
|
+
export function getEntryLockState({ owner }, _optional = {}) {
|
|
10
|
+
let locks = ownedLocks.get(owner);
|
|
11
|
+
if (!locks) {
|
|
12
|
+
locks = createEntryLockState({});
|
|
13
|
+
ownedLocks.set(owner, locks);
|
|
14
|
+
}
|
|
15
|
+
return locks;
|
|
16
|
+
}
|
|
6
17
|
const defaultLocks = createEntryLockState({});
|
|
7
18
|
/**
|
|
8
19
|
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
@@ -29,7 +29,10 @@ import type { ContentTypeRepoPort } from "../content-types/index.js";
|
|
|
29
29
|
import type { EntryRecord, EntryRepoPort } from "../entries/index.js";
|
|
30
30
|
import type { WidgetHostPorts, BeforeSaveHookPort, PostRecord, ForgetRemovedPostFn, PostRepoPort } from "./host-ports.js";
|
|
31
31
|
import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
|
|
32
|
+
import { type EntryLockState } from "./concurrency.js";
|
|
32
33
|
export interface EmbedServiceDeps {
|
|
34
|
+
/** One store-owned state shared by write, embed and region services; omission preserves compatibility. */
|
|
35
|
+
entryLocks?: EntryLockState;
|
|
33
36
|
host: Pick<WidgetHostPorts, "entries" | "entryRefs" | "posts">;
|
|
34
37
|
entryRepo: EntryRepoPort;
|
|
35
38
|
contentTypeRepo: ContentTypeRepoPort;
|
|
@@ -381,7 +381,7 @@ export async function insertWidgetEmbed(required, _optional = {}) {
|
|
|
381
381
|
? await writeHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson)
|
|
382
382
|
: await writePostHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson);
|
|
383
383
|
return { entry, placementId };
|
|
384
|
-
} });
|
|
384
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
385
385
|
}
|
|
386
386
|
/** REQ-44/45: removes the `widgetEmbed` node matching `placementId` from the host entry's body —
|
|
387
387
|
* the placement itself is removed (the widget instance it referenced is untouched). */
|
|
@@ -407,7 +407,7 @@ export async function removeWidgetEmbed(required, _optional = {}) {
|
|
|
407
407
|
? await writeHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson)
|
|
408
408
|
: await writePostHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson);
|
|
409
409
|
return { entry };
|
|
410
|
-
} });
|
|
410
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
411
411
|
}
|
|
412
412
|
export class WidgetEmbedReorderCountMismatchError extends Error {
|
|
413
413
|
expectedCount;
|
|
@@ -455,6 +455,6 @@ export async function reorderWidgetEmbeds(required, _optional = {}) {
|
|
|
455
455
|
? await writeHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson)
|
|
456
456
|
: await writePostHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson);
|
|
457
457
|
return { entry };
|
|
458
|
-
} });
|
|
458
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
459
459
|
}
|
|
460
460
|
//# sourceMappingURL=embed-service.js.map
|
|
@@ -24,9 +24,12 @@ import type { EntryRefsRepoPort } from "./ports.js";
|
|
|
24
24
|
import type { ContentTypeRepoPort } from "../content-types/index.js";
|
|
25
25
|
import type { EntryListPort, EntryRepoPort } from "../entries/index.js";
|
|
26
26
|
import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
|
|
27
|
+
import { type EntryLockState } from "./concurrency.js";
|
|
27
28
|
import type { WidgetRegionBindingRepoPort } from "./ports.js";
|
|
28
29
|
import type { WidgetAreaEntry, WidgetPlacementNode, WidgetRegionKey } from "./types.js";
|
|
29
30
|
export interface RegionAreaServiceDeps {
|
|
31
|
+
/** One store-owned state shared by write, embed and region services; omission preserves compatibility. */
|
|
32
|
+
entryLocks?: EntryLockState;
|
|
30
33
|
host: Pick<WidgetHostPorts, "entries" | "contentTypes" | "entryRefs">;
|
|
31
34
|
entryRepo: EntryRepoPort & EntryListPort;
|
|
32
35
|
contentTypeRepo: ContentTypeRepoPort;
|
|
@@ -78,7 +78,7 @@ export async function bindWidgetArea(required, _optional = {}) {
|
|
|
78
78
|
updatedAt: clockNowIso({ clock: deps.clock }),
|
|
79
79
|
});
|
|
80
80
|
return { areaEntry: toWidgetAreaEntry({ entry: entry }) };
|
|
81
|
-
} });
|
|
81
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
82
82
|
}
|
|
83
83
|
/** REQ-16/AC-10: every referenced widgetEntryId must exist, be a live (non-trashed) widget instance,
|
|
84
84
|
* in this same workspace — checked before any write. Throws the first violation found. */
|
|
@@ -142,7 +142,7 @@ export async function mutateWidgetAreaPlacements(required, _optional = {}) {
|
|
|
142
142
|
updatedAt: clockNowIso({ clock: deps.clock }),
|
|
143
143
|
});
|
|
144
144
|
return { areaEntry: toWidgetAreaEntry({ entry: result.value.entry }) };
|
|
145
|
-
} });
|
|
145
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
148
|
* REQ-12/14: reconciles `widget_region_bindings` from the authoritative `widget_area` entries. The
|
|
@@ -29,10 +29,13 @@ import type { EntryRefsRepoPort } from "./ports.js";
|
|
|
29
29
|
import type { ContentTypeRepoPort } from "../content-types/index.js";
|
|
30
30
|
import type { EntryListPort, EntryRepoPort } from "../entries/index.js";
|
|
31
31
|
import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
|
|
32
|
+
import { type EntryLockState } from "./concurrency.js";
|
|
32
33
|
import type { RemoveWidgetFn } from "./ports.js";
|
|
33
34
|
import type { WidgetInstanceEntry, WidgetTypeKey } from "./types.js";
|
|
34
35
|
import type { WidgetHostPorts } from "./host-ports.js";
|
|
35
36
|
export interface WidgetWriteServiceDeps {
|
|
37
|
+
/** One store-owned state shared by write, embed and region services; omission preserves compatibility. */
|
|
38
|
+
entryLocks?: EntryLockState;
|
|
36
39
|
host: Pick<WidgetHostPorts, "entries" | "contentTypes" | "entryRefs" | "slugify">;
|
|
37
40
|
entryRepo: EntryRepoPort & EntryListPort;
|
|
38
41
|
contentTypeRepo: ContentTypeRepoPort;
|
|
@@ -128,7 +131,7 @@ export interface WidgetTrashDeps extends WidgetWriteServiceDeps {
|
|
|
128
131
|
remove: RemoveWidgetFn;
|
|
129
132
|
}
|
|
130
133
|
export interface TrashWidgetInstanceRequired {
|
|
131
|
-
deps: Pick<WidgetTrashDeps, "entryRepo" | "clock" | "authorize" | "remove">;
|
|
134
|
+
deps: Pick<WidgetTrashDeps, "entryRepo" | "clock" | "authorize" | "remove" | "entryLocks">;
|
|
132
135
|
input: TrashWidgetInstanceInput;
|
|
133
136
|
}
|
|
134
137
|
/**
|
|
@@ -135,7 +135,7 @@ export async function updateWidgetInstance(required, _optional = {}) {
|
|
|
135
135
|
throw result.error;
|
|
136
136
|
}
|
|
137
137
|
return { instance: toWidgetInstanceEntry({ entry: result.value.entry }) };
|
|
138
|
-
} });
|
|
138
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
141
|
* Publish-content's widget write: create-or-update at a caller-given id, with the same permission,
|
|
@@ -182,7 +182,7 @@ export async function importWidgetInstance(required, _optional = {}) {
|
|
|
182
182
|
if (!result.ok)
|
|
183
183
|
throw result.error;
|
|
184
184
|
return { instance: toWidgetInstanceEntry({ entry: result.value.entry }) };
|
|
185
|
-
} });
|
|
185
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
188
|
* Deleting a widget = moving it to the Trash (the injected `remove`). UNCONDITIONAL, per ADR-047 §7
|
|
@@ -226,7 +226,7 @@ export async function trashWidgetInstance(required, _optional = {}) {
|
|
|
226
226
|
if (!fresh)
|
|
227
227
|
throw notFound();
|
|
228
228
|
throw new WidgetVersionConflictError({ message: `widget instance '${input.widgetInstanceId}' changed while it was being deleted — reload and try again`, currentVersion: fresh.version });
|
|
229
|
-
} });
|
|
229
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
230
230
|
}
|
|
231
231
|
/** The payload statuses the old delete ladder left behind: `trash` (step 1) and `purged` (its
|
|
232
232
|
* "delete permanently", which never removed the row). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/cms",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
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",
|
|
@@ -412,8 +418,8 @@
|
|
|
412
418
|
"@types/better-sqlite3": "^7.6.13",
|
|
413
419
|
"@electric-sql/pglite": "0.5.8",
|
|
414
420
|
"jsdom": "^25.0.1",
|
|
415
|
-
"@jini-ai/core": "0.4.4",
|
|
416
421
|
"@jini-ai/db": "0.3.3",
|
|
422
|
+
"@jini-ai/core": "0.4.4",
|
|
417
423
|
"@jini-ai/http-kit": "0.4.1"
|
|
418
424
|
},
|
|
419
425
|
"dependencies": {
|