@jini-ai/cms 0.3.6 → 0.3.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/dist/content-types/index-provisioning.d.ts +1 -1
- package/dist/content-types/index-provisioning.js +1 -1
- package/dist/content-types/write-service.js +31 -11
- package/dist/core/entity-liveness.d.ts +55 -0
- package/dist/core/entity-liveness.js +38 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/ports.d.ts +1 -1
- package/dist/core/tools/registration-kit.d.ts +55 -17
- package/dist/core/tools/registration-kit.js +49 -20
- package/dist/identity/auth-service.d.ts +1 -1
- package/dist/identity/auth-service.js +1 -1
- package/dist/identity/seed.d.ts +1 -1
- package/dist/identity/seed.js +3 -3
- package/dist/media/agent-tools.d.ts +13 -3
- package/dist/media/agent-tools.js +45 -10
- package/dist/media/html-attributes.d.ts +6 -6
- package/dist/media/html-attributes.js +6 -6
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/media-service.d.ts +2 -1
- package/dist/media/media-service.js +23 -7
- package/dist/media/tool-registrations.d.ts +12 -0
- package/dist/media/tool-registrations.js +20 -12
- package/dist/media/types.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/index.js +2 -2
- package/dist/navigation/menu-service.d.ts +4 -4
- package/dist/navigation/menu-service.js +4 -4
- package/dist/settings/dictionaries/index.d.ts +3 -3
- package/dist/settings/dictionaries/index.js +3 -3
- package/dist/taxonomy/agent-tools.d.ts +12 -17
- package/dist/taxonomy/agent-tools.js +53 -29
- package/dist/taxonomy/content-lookup.d.ts +35 -0
- package/dist/taxonomy/content-lookup.js +28 -0
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/index.js +2 -2
- package/dist/taxonomy/repo.memory.d.ts +11 -2
- package/dist/taxonomy/repo.memory.js +11 -0
- package/dist/taxonomy/write-service.d.ts +44 -15
- package/dist/taxonomy/write-service.js +60 -8
- package/package.json +3 -2
- package/dist/content-types/agent-tools.d.ts.map +0 -1
- package/dist/content-types/agent-tools.js.map +0 -1
- package/dist/content-types/cleanup.d.ts.map +0 -1
- package/dist/content-types/cleanup.js.map +0 -1
- package/dist/content-types/errors.d.ts.map +0 -1
- package/dist/content-types/errors.js.map +0 -1
- package/dist/content-types/field-defs.d.ts.map +0 -1
- package/dist/content-types/field-defs.js.map +0 -1
- package/dist/content-types/index-provisioning.d.ts.map +0 -1
- package/dist/content-types/index-provisioning.js.map +0 -1
- package/dist/content-types/index.d.ts.map +0 -1
- package/dist/content-types/index.js.map +0 -1
- package/dist/content-types/lifecycle-dispatch.d.ts.map +0 -1
- package/dist/content-types/lifecycle-dispatch.js.map +0 -1
- package/dist/content-types/lifecycle.d.ts.map +0 -1
- package/dist/content-types/lifecycle.js.map +0 -1
- package/dist/content-types/list.d.ts.map +0 -1
- package/dist/content-types/list.js.map +0 -1
- package/dist/content-types/repo.memory.d.ts.map +0 -1
- package/dist/content-types/repo.memory.js.map +0 -1
- package/dist/content-types/tool-registrations.d.ts.map +0 -1
- package/dist/content-types/tool-registrations.js.map +0 -1
- package/dist/content-types/types.d.ts.map +0 -1
- package/dist/content-types/types.js.map +0 -1
- package/dist/content-types/write-service.d.ts.map +0 -1
- package/dist/content-types/write-service.js.map +0 -1
- package/dist/core/commands/change-set.d.ts.map +0 -1
- package/dist/core/commands/change-set.js.map +0 -1
- package/dist/core/commands/command.d.ts.map +0 -1
- package/dist/core/commands/command.js.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/ports.d.ts.map +0 -1
- package/dist/core/ports.js.map +0 -1
- package/dist/core/tools/registration-kit.d.ts.map +0 -1
- package/dist/core/tools/registration-kit.js.map +0 -1
- package/dist/entries/agent-tools.d.ts.map +0 -1
- package/dist/entries/agent-tools.js.map +0 -1
- package/dist/entries/errors.d.ts.map +0 -1
- package/dist/entries/errors.js.map +0 -1
- package/dist/entries/field-validation.d.ts.map +0 -1
- package/dist/entries/field-validation.js.map +0 -1
- package/dist/entries/index.d.ts.map +0 -1
- package/dist/entries/index.js.map +0 -1
- package/dist/entries/lifecycle-dispatch.d.ts.map +0 -1
- package/dist/entries/lifecycle-dispatch.js.map +0 -1
- package/dist/entries/list.d.ts.map +0 -1
- package/dist/entries/list.js.map +0 -1
- package/dist/entries/repo.memory.d.ts.map +0 -1
- package/dist/entries/repo.memory.js.map +0 -1
- package/dist/entries/tool-registrations.d.ts.map +0 -1
- package/dist/entries/tool-registrations.js.map +0 -1
- package/dist/entries/types.d.ts.map +0 -1
- package/dist/entries/types.js.map +0 -1
- package/dist/entries/write-service.d.ts.map +0 -1
- package/dist/entries/write-service.js.map +0 -1
- package/dist/identity/admin-crud-service.d.ts.map +0 -1
- package/dist/identity/admin-crud-service.js.map +0 -1
- package/dist/identity/agent-tool-input.d.ts.map +0 -1
- package/dist/identity/agent-tool-input.js.map +0 -1
- package/dist/identity/agent-tools.d.ts.map +0 -1
- package/dist/identity/agent-tools.js.map +0 -1
- package/dist/identity/auth-service.d.ts.map +0 -1
- package/dist/identity/auth-service.js.map +0 -1
- package/dist/identity/authorize.d.ts.map +0 -1
- package/dist/identity/authorize.js.map +0 -1
- package/dist/identity/grant-service.d.ts.map +0 -1
- package/dist/identity/grant-service.js.map +0 -1
- package/dist/identity/hasher.d.ts.map +0 -1
- package/dist/identity/hasher.js.map +0 -1
- package/dist/identity/index.d.ts.map +0 -1
- package/dist/identity/index.js.map +0 -1
- package/dist/identity/password-policy.d.ts.map +0 -1
- package/dist/identity/password-policy.js.map +0 -1
- package/dist/identity/permission-migrations.d.ts.map +0 -1
- package/dist/identity/permission-migrations.js.map +0 -1
- package/dist/identity/permissions.d.ts.map +0 -1
- package/dist/identity/permissions.js.map +0 -1
- package/dist/identity/ports.d.ts.map +0 -1
- package/dist/identity/ports.js.map +0 -1
- package/dist/identity/repo.memory.d.ts.map +0 -1
- package/dist/identity/repo.memory.js.map +0 -1
- package/dist/identity/seed.d.ts.map +0 -1
- package/dist/identity/seed.js.map +0 -1
- package/dist/identity/tool-registrations.d.ts.map +0 -1
- package/dist/identity/tool-registrations.js.map +0 -1
- package/dist/identity/types.d.ts.map +0 -1
- package/dist/identity/types.js.map +0 -1
- package/dist/identity/username.d.ts.map +0 -1
- package/dist/identity/username.js.map +0 -1
- package/dist/media/agent-tools.d.ts.map +0 -1
- package/dist/media/agent-tools.js.map +0 -1
- package/dist/media/blob-gc-lock.d.ts.map +0 -1
- package/dist/media/blob-gc-lock.js.map +0 -1
- package/dist/media/blob-gc.d.ts.map +0 -1
- package/dist/media/blob-gc.js.map +0 -1
- package/dist/media/blob-key.d.ts.map +0 -1
- package/dist/media/blob-key.js.map +0 -1
- package/dist/media/blob-store.fs.d.ts.map +0 -1
- package/dist/media/blob-store.fs.js.map +0 -1
- package/dist/media/blob-store.memory.d.ts.map +0 -1
- package/dist/media/blob-store.memory.js.map +0 -1
- package/dist/media/content-type-sniffer.d.ts.map +0 -1
- package/dist/media/content-type-sniffer.js.map +0 -1
- package/dist/media/html-attributes.d.ts.map +0 -1
- package/dist/media/html-attributes.js.map +0 -1
- package/dist/media/image-transformer.d.ts.map +0 -1
- package/dist/media/image-transformer.js.map +0 -1
- package/dist/media/image-transformer.sharp.d.ts.map +0 -1
- package/dist/media/image-transformer.sharp.js.map +0 -1
- package/dist/media/index.d.ts.map +0 -1
- package/dist/media/index.js.map +0 -1
- package/dist/media/media-service.d.ts.map +0 -1
- package/dist/media/media-service.js.map +0 -1
- package/dist/media/ports.d.ts.map +0 -1
- package/dist/media/ports.js.map +0 -1
- package/dist/media/rendition-service.d.ts.map +0 -1
- package/dist/media/rendition-service.js.map +0 -1
- package/dist/media/repo.memory.d.ts.map +0 -1
- package/dist/media/repo.memory.js.map +0 -1
- package/dist/media/tool-registrations.d.ts.map +0 -1
- package/dist/media/tool-registrations.js.map +0 -1
- package/dist/media/transform-lock.d.ts.map +0 -1
- package/dist/media/transform-lock.js.map +0 -1
- package/dist/media/transform-registry.d.ts.map +0 -1
- package/dist/media/transform-registry.js.map +0 -1
- package/dist/media/transform-types.d.ts.map +0 -1
- package/dist/media/transform-types.js.map +0 -1
- package/dist/media/types.d.ts.map +0 -1
- package/dist/media/types.js.map +0 -1
- package/dist/navigation/agent-tools.d.ts.map +0 -1
- package/dist/navigation/agent-tools.js.map +0 -1
- package/dist/navigation/contracts.d.ts.map +0 -1
- package/dist/navigation/contracts.js.map +0 -1
- package/dist/navigation/index.d.ts.map +0 -1
- package/dist/navigation/index.js.map +0 -1
- package/dist/navigation/menu-service.d.ts.map +0 -1
- package/dist/navigation/menu-service.js.map +0 -1
- package/dist/navigation/ports.d.ts.map +0 -1
- package/dist/navigation/ports.js.map +0 -1
- package/dist/navigation/read-model.d.ts.map +0 -1
- package/dist/navigation/read-model.js.map +0 -1
- package/dist/navigation/reconcile.d.ts.map +0 -1
- package/dist/navigation/reconcile.js.map +0 -1
- package/dist/navigation/repo.memory.d.ts.map +0 -1
- package/dist/navigation/repo.memory.js.map +0 -1
- package/dist/navigation/resolver.d.ts.map +0 -1
- package/dist/navigation/resolver.js.map +0 -1
- package/dist/navigation/tool-registrations.d.ts.map +0 -1
- package/dist/navigation/tool-registrations.js.map +0 -1
- package/dist/navigation/types.d.ts.map +0 -1
- package/dist/navigation/types.js.map +0 -1
- package/dist/presentation/index.d.ts.map +0 -1
- package/dist/presentation/index.js.map +0 -1
- package/dist/presentation/presentation.d.ts.map +0 -1
- package/dist/presentation/presentation.js.map +0 -1
- package/dist/presentation/repo.memory.d.ts.map +0 -1
- package/dist/presentation/repo.memory.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/settings/agent-tools.d.ts.map +0 -1
- package/dist/settings/agent-tools.js.map +0 -1
- package/dist/settings/agent-writable-preferences.d.ts.map +0 -1
- package/dist/settings/agent-writable-preferences.js.map +0 -1
- package/dist/settings/change-feed.d.ts.map +0 -1
- package/dist/settings/change-feed.js.map +0 -1
- package/dist/settings/definitions-dispatch.d.ts.map +0 -1
- package/dist/settings/definitions-dispatch.js.map +0 -1
- package/dist/settings/dictionaries/index.d.ts.map +0 -1
- package/dist/settings/dictionaries/index.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ar.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js.map +0 -1
- package/dist/settings/ensure-definitions.d.ts.map +0 -1
- package/dist/settings/ensure-definitions.js.map +0 -1
- package/dist/settings/errors.d.ts.map +0 -1
- package/dist/settings/errors.js.map +0 -1
- package/dist/settings/index.d.ts.map +0 -1
- package/dist/settings/index.js.map +0 -1
- package/dist/settings/ports.d.ts.map +0 -1
- package/dist/settings/ports.js.map +0 -1
- package/dist/settings/purge-service.d.ts.map +0 -1
- package/dist/settings/purge-service.js.map +0 -1
- package/dist/settings/repo.memory.d.ts.map +0 -1
- package/dist/settings/repo.memory.js.map +0 -1
- package/dist/settings/settings.d.ts.map +0 -1
- package/dist/settings/settings.js.map +0 -1
- package/dist/settings/tool-registrations.d.ts.map +0 -1
- package/dist/settings/tool-registrations.js.map +0 -1
- package/dist/settings/types.d.ts.map +0 -1
- package/dist/settings/types.js.map +0 -1
- package/dist/settings/ui-tab-definitions.d.ts.map +0 -1
- package/dist/settings/ui-tab-definitions.js.map +0 -1
- package/dist/settings/write-service.d.ts.map +0 -1
- package/dist/settings/write-service.js.map +0 -1
- package/dist/taxonomy/agent-tools.d.ts.map +0 -1
- package/dist/taxonomy/agent-tools.js.map +0 -1
- package/dist/taxonomy/content-lookup.d.ts.map +0 -1
- package/dist/taxonomy/content-lookup.js.map +0 -1
- package/dist/taxonomy/index.d.ts.map +0 -1
- package/dist/taxonomy/index.js.map +0 -1
- package/dist/taxonomy/list.d.ts.map +0 -1
- package/dist/taxonomy/list.js.map +0 -1
- package/dist/taxonomy/merge-term.d.ts.map +0 -1
- package/dist/taxonomy/merge-term.js.map +0 -1
- package/dist/taxonomy/repo.memory.d.ts.map +0 -1
- package/dist/taxonomy/repo.memory.js.map +0 -1
- package/dist/taxonomy/validation-chain.d.ts.map +0 -1
- package/dist/taxonomy/validation-chain.js.map +0 -1
- package/dist/taxonomy/write-service.d.ts.map +0 -1
- package/dist/taxonomy/write-service.js.map +0 -1
- package/dist/workspace/agent-tools.d.ts.map +0 -1
- package/dist/workspace/agent-tools.js.map +0 -1
- package/dist/workspace/create.d.ts.map +0 -1
- package/dist/workspace/create.js.map +0 -1
- package/dist/workspace/delete.d.ts.map +0 -1
- package/dist/workspace/delete.js.map +0 -1
- package/dist/workspace/index.d.ts.map +0 -1
- package/dist/workspace/index.js.map +0 -1
- package/dist/workspace/repo.memory.d.ts.map +0 -1
- package/dist/workspace/repo.memory.js.map +0 -1
- package/dist/workspace/tool-registrations.d.ts.map +0 -1
- package/dist/workspace/tool-registrations.js.map +0 -1
- package/dist/workspace/update.d.ts.map +0 -1
- package/dist/workspace/update.js.map +0 -1
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Server-side allowlist for `MediaRecord.htmlAttributes` (2026-09-07) — the enforcement half
|
|
3
|
-
* of a validator that ALSO has a browser-side copy in
|
|
3
|
+
* of a validator that ALSO has a browser-side copy in the consuming product's admin
|
|
4
4
|
* (`apps/admin/src/features/media/rules.ts`, `parseMediaHtmlAttributes` and friends).
|
|
5
5
|
*
|
|
6
6
|
* Deliberately duplicated, not shared via one imported module, for the same reason this codebase
|
|
7
|
-
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into
|
|
7
|
+
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into that admin's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
|
|
8
8
|
* `IMPORTABLE_CONTENT_TYPES`: `@jini-ai/cms/media`'s barrel (`index.ts`) re-exports files that
|
|
9
9
|
* import real Node built-ins (`node:crypto` in `media-service.ts`, `node:fs` in
|
|
10
10
|
* `blob-store.fs.ts`, native `sharp` bindings in `image-transformer.sharp.ts`) — importing this
|
|
11
|
-
* subpath from a browser-bundled Vite app (
|
|
11
|
+
* subpath from a browser-bundled Vite app (that product's `apps/admin`) risks pulling those into the admin
|
|
12
12
|
* SPA's bundle even if only this one pure function is actually used, and this admin's own copy
|
|
13
13
|
* already exists, tested, with no such risk. This file is the copy every Node-side consumer
|
|
14
|
-
* (`media-service.ts`'s own `updateMediaMetadata`, and
|
|
14
|
+
* (`media-service.ts`'s own `updateMediaMetadata`, and that product's `apps/website` render path, both pure
|
|
15
15
|
* backend code) can safely import from `@jini-ai/cms/media` instead of re-deriving the rules a
|
|
16
16
|
* third time.
|
|
17
17
|
*
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* needed — that is the one piece of drift risk this duplication accepts, the same risk every other
|
|
22
22
|
* hand-copied list in this codebase already accepts.
|
|
23
23
|
*
|
|
24
|
-
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in
|
|
24
|
+
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in the consuming product's admin
|
|
25
25
|
* but rendered on the public site, so a free-text HTML-attribute passthrough is a stored-XSS vector
|
|
26
26
|
* (`onerror`, `onclick`, `style`, `href="javascript:"`, any `on*` handler) the moment it reaches a
|
|
27
27
|
* public page. A client-side-only check is not a control, since the API accepts whatever a caller
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* families (checked separately in {@link isAllowedMediaHtmlAttributeName}). Picked for the owner's
|
|
34
34
|
* stated near-term uses (animations, custom WebMCP hooks) plus the standard `<img>`/`<video>`
|
|
35
35
|
* attributes those uses actually need; extend this list, not the parser, when a new one is needed —
|
|
36
|
-
* and keep it in sync with the identical constant in
|
|
36
|
+
* and keep it in sync with the identical constant in that admin's copy (see this file's header).
|
|
37
37
|
*/
|
|
38
38
|
export declare const MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES: readonly ["loading", "decoding", "playsinline", "muted", "loop", "autoplay", "poster"];
|
|
39
39
|
/** Whether `name` is on the allowlist — a well-formed attribute name
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Server-side allowlist for `MediaRecord.htmlAttributes` (2026-09-07) — the enforcement half
|
|
3
|
-
* of a validator that ALSO has a browser-side copy in
|
|
3
|
+
* of a validator that ALSO has a browser-side copy in the consuming product's admin
|
|
4
4
|
* (`apps/admin/src/features/media/rules.ts`, `parseMediaHtmlAttributes` and friends).
|
|
5
5
|
*
|
|
6
6
|
* Deliberately duplicated, not shared via one imported module, for the same reason this codebase
|
|
7
|
-
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into
|
|
7
|
+
* already hand-copies `DEFAULT_ALLOWED_MIME_TYPES` into that admin's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
|
|
8
8
|
* `IMPORTABLE_CONTENT_TYPES`: `@jini-ai/cms/media`'s barrel (`index.ts`) re-exports files that
|
|
9
9
|
* import real Node built-ins (`node:crypto` in `media-service.ts`, `node:fs` in
|
|
10
10
|
* `blob-store.fs.ts`, native `sharp` bindings in `image-transformer.sharp.ts`) — importing this
|
|
11
|
-
* subpath from a browser-bundled Vite app (
|
|
11
|
+
* subpath from a browser-bundled Vite app (that product's `apps/admin`) risks pulling those into the admin
|
|
12
12
|
* SPA's bundle even if only this one pure function is actually used, and this admin's own copy
|
|
13
13
|
* already exists, tested, with no such risk. This file is the copy every Node-side consumer
|
|
14
|
-
* (`media-service.ts`'s own `updateMediaMetadata`, and
|
|
14
|
+
* (`media-service.ts`'s own `updateMediaMetadata`, and that product's `apps/website` render path, both pure
|
|
15
15
|
* backend code) can safely import from `@jini-ai/cms/media` instead of re-deriving the rules a
|
|
16
16
|
* third time.
|
|
17
17
|
*
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* needed — that is the one piece of drift risk this duplication accepts, the same risk every other
|
|
22
22
|
* hand-copied list in this codebase already accepts.
|
|
23
23
|
*
|
|
24
|
-
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in
|
|
24
|
+
* This is a SECURITY boundary, not a syntax convenience: media metadata is authored in the consuming product's admin
|
|
25
25
|
* but rendered on the public site, so a free-text HTML-attribute passthrough is a stored-XSS vector
|
|
26
26
|
* (`onerror`, `onclick`, `style`, `href="javascript:"`, any `on*` handler) the moment it reaches a
|
|
27
27
|
* public page. A client-side-only check is not a control, since the API accepts whatever a caller
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* families (checked separately in {@link isAllowedMediaHtmlAttributeName}). Picked for the owner's
|
|
34
34
|
* stated near-term uses (animations, custom WebMCP hooks) plus the standard `<img>`/`<video>`
|
|
35
35
|
* attributes those uses actually need; extend this list, not the parser, when a new one is needed —
|
|
36
|
-
* and keep it in sync with the identical constant in
|
|
36
|
+
* and keep it in sync with the identical constant in that admin's copy (see this file's header).
|
|
37
37
|
*/
|
|
38
38
|
export const MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES = [
|
|
39
39
|
"loading",
|
package/dist/media/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { withSha256Lock } from "./blob-gc-lock.js";
|
|
|
21
21
|
export { DEFAULT_GC_GRACE_MS, resolveGcGraceMs, isBlobUnreferenced, tombstoneBlobIfUnreferenced, runBlobGcDeletePass, runBlobGcUnlinkPass, runBlobGcCycle, runMonthlyOrphanSweepStub, } from "./blob-gc.js";
|
|
22
22
|
export { InMemoryBlobStore } from "./blob-store.memory.js";
|
|
23
23
|
export { LocalFsBlobStore, type LocalFsBlobStoreDeps } from "./blob-store.fs.js";
|
|
24
|
-
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, type UploadMediaInput, type UploadMediaDeps, type UpdateMediaMetadataInput, type FindMediaByIdOrSlugRequired, } from "./media-service.js";
|
|
24
|
+
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, isValidMediaSlugFormat, type UploadMediaInput, type UploadMediaDeps, type UpdateMediaMetadataInput, type FindMediaByIdOrSlugRequired, } from "./media-service.js";
|
|
25
25
|
export type { TransformFit, TransformFormat, TransformParams, TransformDefinitionRecord, } from "./transform-types.js";
|
|
26
26
|
export { TransformValidationError, mimeForTransformFormat, MAX_TRANSFORM_DIMENSION_PX } from "./transform-types.js";
|
|
27
27
|
export { withRenditionLock, withTransformRegistryLock } from "./transform-lock.js";
|
package/dist/media/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { withSha256Lock } from "./blob-gc-lock.js";
|
|
|
6
6
|
export { DEFAULT_GC_GRACE_MS, resolveGcGraceMs, isBlobUnreferenced, tombstoneBlobIfUnreferenced, runBlobGcDeletePass, runBlobGcUnlinkPass, runBlobGcCycle, runMonthlyOrphanSweepStub, } from "./blob-gc.js";
|
|
7
7
|
export { InMemoryBlobStore } from "./blob-store.memory.js";
|
|
8
8
|
export { LocalFsBlobStore } from "./blob-store.fs.js";
|
|
9
|
-
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, } from "./media-service.js";
|
|
9
|
+
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, isValidMediaSlugFormat, } from "./media-service.js";
|
|
10
10
|
export { TransformValidationError, mimeForTransformFormat, MAX_TRANSFORM_DIMENSION_PX } from "./transform-types.js";
|
|
11
11
|
export { withRenditionLock, withTransformRegistryLock } from "./transform-lock.js";
|
|
12
12
|
export { registerTransform, getLatestTransformDefinition, isLatestTransformVersion, isReferencedByPublishedContent, } from "./transform-registry.js";
|
|
@@ -23,7 +23,7 @@ export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
|
23
23
|
* image types above, an accepted video is never re-encoded — there is no
|
|
24
24
|
* `TransformFormat` for video (`transform-types.ts`'s union is image-only), so
|
|
25
25
|
* a video asset's public URL bypasses the transform/rendition pipeline
|
|
26
|
-
* entirely and serves the original bytes as-is (host concern; see
|
|
26
|
+
* entirely and serves the original bytes as-is (host concern; see the host's
|
|
27
27
|
* `routes/site/media-rendition.ts`). This widens the SAME advisory,
|
|
28
28
|
* client-declared-string check item 4 above already discloses as untrusted —
|
|
29
29
|
* no new ingress hardening was added for video.
|
|
@@ -49,6 +49,7 @@ export declare function resolveWriteOnceSource(required: {
|
|
|
49
49
|
existing: MediaSource | undefined;
|
|
50
50
|
requestedSha256: string;
|
|
51
51
|
}, _optional?: Record<string, never>): MediaSource;
|
|
52
|
+
export declare function isValidMediaSlugFormat(slug: string): boolean;
|
|
52
53
|
export interface UploadMediaInput {
|
|
53
54
|
workspaceId: UUID;
|
|
54
55
|
bytes: Uint8Array;
|
|
@@ -58,6 +58,22 @@ import { describeMediaHtmlAttributeError, parseMediaHtmlAttributes } from "./htm
|
|
|
58
58
|
export { MediaConflictError, MediaNotFoundError, MediaSourceImmutableError, MediaStillReferencedError, MediaValidationError, };
|
|
59
59
|
/** Default byte-size cap (this pass's stand-in for the real ingress policy's streaming cap). */
|
|
60
60
|
export const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
61
|
+
/**
|
|
62
|
+
* Renders a byte count as a human-readable MB figure for the size-cap rejection below (2026-09-21).
|
|
63
|
+
* Every real caller passes a MiB-scale `maxUploadBytes` (10 MiB default, or a host override such as
|
|
64
|
+
* Tovu's 50 MiB), so one decimal place is enough precision without ever showing a raw byte count —
|
|
65
|
+
* the whole point of this fix (see `uploadMedia`'s size-cap check, and `agent-tools.ts`'s file header
|
|
66
|
+
* for why the catalog description can no longer state a specific number either). `toFixed(1)`'s
|
|
67
|
+
* trailing `.0` is trimmed so a clean multiple of 1 MB (the common case) reads as `10 MB`, not `10.0
|
|
68
|
+
* MB`.
|
|
69
|
+
*
|
|
70
|
+
* @complexity O(1).
|
|
71
|
+
*/
|
|
72
|
+
function formatMaxUploadBytesAsMb(bytes) {
|
|
73
|
+
const mb = bytes / (1024 * 1024);
|
|
74
|
+
const rounded = mb.toFixed(1);
|
|
75
|
+
return `${rounded.endsWith(".0") ? rounded.slice(0, -2) : rounded} MB`;
|
|
76
|
+
}
|
|
61
77
|
/**
|
|
62
78
|
* Advisory MIME allowlist. SVG is deliberately EXCLUDED (not "TODO, forgot") —
|
|
63
79
|
* SVG must be sanitized at ingest before it's safe to store;
|
|
@@ -77,7 +93,7 @@ export const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
|
77
93
|
* image types above, an accepted video is never re-encoded — there is no
|
|
78
94
|
* `TransformFormat` for video (`transform-types.ts`'s union is image-only), so
|
|
79
95
|
* a video asset's public URL bypasses the transform/rendition pipeline
|
|
80
|
-
* entirely and serves the original bytes as-is (host concern; see
|
|
96
|
+
* entirely and serves the original bytes as-is (host concern; see the host's
|
|
81
97
|
* `routes/site/media-rendition.ts`). This widens the SAME advisory,
|
|
82
98
|
* client-declared-string check item 4 above already discloses as untrusted —
|
|
83
99
|
* no new ingress hardening was added for video.
|
|
@@ -150,7 +166,7 @@ const MEDIA_SLUG_UUID_SHAPE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
|
|
|
150
166
|
* human for a shorter value — silently truncating an internally-derived value is the same
|
|
151
167
|
* "coerce, don't fail" convention `deriveTitleFromFilename`/`slugifyMediaTitle` already use. */
|
|
152
168
|
const MEDIA_MAX_SLUG_LENGTH = 120;
|
|
153
|
-
function isValidMediaSlugFormat(slug) {
|
|
169
|
+
export function isValidMediaSlugFormat(slug) {
|
|
154
170
|
return MEDIA_SLUG_FORMAT_PATTERN.test(slug) && !MEDIA_SLUG_UUID_SHAPE_PATTERN.test(slug);
|
|
155
171
|
}
|
|
156
172
|
/** Truncates a slug candidate to `maxLength`, stripping a trailing dash the cut can introduce (so a
|
|
@@ -165,9 +181,9 @@ function capSlugCandidate(candidate, maxLength) {
|
|
|
165
181
|
}
|
|
166
182
|
/** Turns free text into a slug candidate: lowercase, non-alphanumeric runs collapsed to one dash,
|
|
167
183
|
* leading/trailing dashes trimmed. Same algorithm as `post.ts`'s private `slugify` (duplicated, not
|
|
168
|
-
* imported — cross-repo: `post.ts` lives in
|
|
184
|
+
* imported — cross-repo: `post.ts` lives in the host, this file in `@jini-ai/cms`; this codebase's own
|
|
169
185
|
* precedent for a small hand-copied helper mirrored across a repo boundary is `DEFAULT_ALLOWED_MIME_TYPES`
|
|
170
|
-
* vs.
|
|
186
|
+
* vs. the host's `FILE_HANDLER_ALLOWED_MIME_TYPES`/`IMPORTABLE_CONTENT_TYPES`). Never throws and never
|
|
171
187
|
* returns `undefined` — an all-punctuation input collapses to `""`, which every caller here treats
|
|
172
188
|
* as "derive nothing, fall back to a fixed default" rather than a malformed-input error. */
|
|
173
189
|
function slugifyMediaTitle(title) {
|
|
@@ -181,12 +197,12 @@ function slugifyMediaTitle(title) {
|
|
|
181
197
|
* Derives a unique-per-workspace slug from `title`, suffixing `-2`, `-3`, … on collision — the
|
|
182
198
|
* identical loop shape `post.ts`'s `createPost` uses for its own derived-slug path. Shared by
|
|
183
199
|
* {@link uploadMedia} (derive-on-create) and the backfill a host runs once for pre-existing rows
|
|
184
|
-
* with no slug yet (see
|
|
200
|
+
* with no slug yet (see the host's `development/scripts/backfill-media-slugs.ts`).
|
|
185
201
|
*
|
|
186
202
|
* `base` falls back to `"untitled"` when `title` slugifies to the empty string (all-punctuation or
|
|
187
203
|
* non-Latin titles that `slugifyMediaTitle` strips to nothing) — `uploadMedia`'s own title is never
|
|
188
204
|
* empty (`deriveTitleFromFilename` guarantees a non-empty string), but the backfill script
|
|
189
|
-
* (`development/scripts/backfill-media-slugs.ts`,
|
|
205
|
+
* (`development/scripts/backfill-media-slugs.ts`, host-side) calls this against arbitrary pre-existing
|
|
190
206
|
* titles, so this fallback is load-bearing there, not just a defensive floor. Note this is the SAME
|
|
191
207
|
* fallback base every empty-slugifying title collapses onto, so two such titles in one workspace
|
|
192
208
|
* collide on `"untitled"` and are disambiguated by the ordinary suffix loop below exactly like any
|
|
@@ -248,7 +264,7 @@ export async function uploadMedia(required, optional = {}) {
|
|
|
248
264
|
throw new MediaValidationError("uploaded file is empty");
|
|
249
265
|
}
|
|
250
266
|
if (input.bytes.byteLength > maxUploadBytes) {
|
|
251
|
-
throw new MediaValidationError(`uploaded file exceeds the ${maxUploadBytes}
|
|
267
|
+
throw new MediaValidationError(`uploaded file exceeds the ${formatMaxUploadBytesAsMb(maxUploadBytes)} size cap`);
|
|
252
268
|
}
|
|
253
269
|
const sha256 = createHash("sha256").update(input.bytes).digest("hex");
|
|
254
270
|
const nowIso = deps.clock.nowIso();
|
|
@@ -92,6 +92,18 @@ export interface MediaToolDeps {
|
|
|
92
92
|
media: MediaRecord;
|
|
93
93
|
bytes: Uint8Array;
|
|
94
94
|
}) => Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Optional host-supplied override of `uploadMedia`'s own `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB,
|
|
97
|
+
* `media-service.ts`), forwarded verbatim as `uploadMedia`'s third (`optional`) argument in the
|
|
98
|
+
* `media_upload_asset` handler below (2026-09-21). Before this field existed, the handler called
|
|
99
|
+
* `uploadMedia({ deps, input })` with no third argument at all, so EVERY host's assistant upload
|
|
100
|
+
* tool was hard-capped at 10 MiB regardless of what limit that host's own HTTP upload route
|
|
101
|
+
* enforced — a host raising its own cap (e.g. to 50 MiB) had no way to raise this tool's cap to
|
|
102
|
+
* match. Omitted: `uploadMedia`'s own default applies unchanged, matching this field's own
|
|
103
|
+
* pre-2026-09-21 absence for every host that has not opted in yet — the same degrade-soft
|
|
104
|
+
* contract {@link resolvePublicUrls} and {@link recordUploadContentType} already established.
|
|
105
|
+
*/
|
|
106
|
+
maxUploadBytes?: number;
|
|
95
107
|
}
|
|
96
108
|
/**
|
|
97
109
|
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
@@ -30,6 +30,7 @@ function isMediaShapeRejection(error) {
|
|
|
30
30
|
function toMediaToolView(record) {
|
|
31
31
|
return {
|
|
32
32
|
id: record.id,
|
|
33
|
+
slug: record.slug,
|
|
33
34
|
title: record.title,
|
|
34
35
|
alt: record.alt,
|
|
35
36
|
caption: record.caption,
|
|
@@ -37,6 +38,8 @@ function toMediaToolView(record) {
|
|
|
37
38
|
sha256: record.source.sha256,
|
|
38
39
|
status: record.status,
|
|
39
40
|
version: record.version,
|
|
41
|
+
cssClass: record.cssClass,
|
|
42
|
+
htmlAttributes: record.htmlAttributes,
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
@@ -92,7 +95,7 @@ export function buildMediaRegistrations(routeDeps) {
|
|
|
92
95
|
credit: typeof input.credit === "string" ? input.credit : undefined,
|
|
93
96
|
createdByPrincipal: ctx.principal.id,
|
|
94
97
|
},
|
|
95
|
-
});
|
|
98
|
+
}, { maxUploadBytes: routeDeps.maxUploadBytes });
|
|
96
99
|
if (routeDeps.recordUploadContentType) {
|
|
97
100
|
try {
|
|
98
101
|
await routeDeps.recordUploadContentType({ media, bytes });
|
|
@@ -121,18 +124,23 @@ export function buildMediaRegistrations(routeDeps) {
|
|
|
121
124
|
const input = requireInputRecord(ctx.input);
|
|
122
125
|
const mediaId = requireString(input, "mediaId");
|
|
123
126
|
await requireToolPermission(routeDeps, { principalId: ctx.principal.id, permission: "media.update", entityType: "media", entityId: mediaId });
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
return withSchemaOnRejection({ toolId: "media_update_metadata", catalog: CATALOG_BY_ID, isShapeRejection: isMediaShapeRejection }, async () => {
|
|
128
|
+
const { media } = await updateMediaMetadata({
|
|
129
|
+
deps: { clock: routeDeps.clock, mediaRepo: routeDeps.mediaRepo },
|
|
130
|
+
input: {
|
|
131
|
+
workspaceId: routeDeps.workspaceId,
|
|
132
|
+
id: mediaId,
|
|
133
|
+
title: typeof input.title === "string" ? input.title : undefined,
|
|
134
|
+
alt: typeof input.alt === "string" ? input.alt : undefined,
|
|
135
|
+
caption: typeof input.caption === "string" ? input.caption : undefined,
|
|
136
|
+
credit: typeof input.credit === "string" ? input.credit : undefined,
|
|
137
|
+
slug: typeof input.slug === "string" ? input.slug : undefined,
|
|
138
|
+
cssClass: typeof input.cssClass === "string" ? input.cssClass : undefined,
|
|
139
|
+
htmlAttributes: typeof input.htmlAttributes === "string" ? input.htmlAttributes : undefined,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return { media: toMediaToolView(media) };
|
|
134
143
|
});
|
|
135
|
-
return { media: toMediaToolView(media) };
|
|
136
144
|
},
|
|
137
145
|
media_trash_asset: async (ctx) => {
|
|
138
146
|
const mediaId = requireString(requireInputRecord(ctx.input), "mediaId");
|
package/dist/media/types.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface MediaRecord {
|
|
|
50
50
|
*
|
|
51
51
|
* Derived from `title` at upload time (`deriveMediaSlug`), then independently editable afterward
|
|
52
52
|
* — renaming `title` does NOT recompute this field; it changes only on an explicit `slug` write.
|
|
53
|
-
* Unique per `(workspaceId, slug)`, enforced by the host's own DB index (see
|
|
53
|
+
* Unique per `(workspaceId, slug)`, enforced by the host's own DB index (see the host's
|
|
54
54
|
* `idx_media_workspace_slug`) — `updateMediaMetadata`'s own `findBySlug` check is a friendly-error
|
|
55
55
|
* courtesy on top of that, not the enforcement itself, the same split `posts_workspace_slug_unique`
|
|
56
56
|
* already establishes for `post`'s identical `slug` field.
|
|
@@ -20,10 +20,10 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, type MenuRepoPort, }
|
|
|
20
20
|
* `isAllowedHref`/`ALLOWED_HREF_SHAPES_DESCRIPTION` (2026-09-03): the canonical author-link
|
|
21
21
|
* allowlist, promoted here specifically so a HOST can import it rather than hand-copy it — the
|
|
22
22
|
* dependency direction a host -> `@jini-ai/cms` is the one that is actually legal (the reverse is
|
|
23
|
-
* not).
|
|
23
|
+
* not). The host's own render-time `safeHref` (`server/inbound/public-http/http/site/render.ts` and its
|
|
24
24
|
* `features/theme/static-render.ts` duplicate) have not yet been migrated to import this — see
|
|
25
25
|
* `menu-service.ts`'s own doc comment on `isAllowedHref` for the retirement plan and why nothing in
|
|
26
|
-
* `check:boundaries` blocks either
|
|
26
|
+
* `check:boundaries` blocks either host-side call site from doing so.
|
|
27
27
|
*/
|
|
28
28
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, 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
|
export { resolveForLocation, resolveMenuDoc, type ResolveTargetHrefFn, type ResolvedTargetHref, type ResolveForLocationDeps, type ResolveForLocationServiceInput, type ResolveForLocationRequired, type ResolveForLocationOptional, } from "./resolver.js";
|
package/dist/navigation/index.js
CHANGED
|
@@ -11,10 +11,10 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, } from "./repo.memory
|
|
|
11
11
|
* `isAllowedHref`/`ALLOWED_HREF_SHAPES_DESCRIPTION` (2026-09-03): the canonical author-link
|
|
12
12
|
* allowlist, promoted here specifically so a HOST can import it rather than hand-copy it — the
|
|
13
13
|
* dependency direction a host -> `@jini-ai/cms` is the one that is actually legal (the reverse is
|
|
14
|
-
* not).
|
|
14
|
+
* not). The host's own render-time `safeHref` (`server/inbound/public-http/http/site/render.ts` and its
|
|
15
15
|
* `features/theme/static-render.ts` duplicate) have not yet been migrated to import this — see
|
|
16
16
|
* `menu-service.ts`'s own doc comment on `isAllowedHref` for the retirement plan and why nothing in
|
|
17
|
-
* `check:boundaries` blocks either
|
|
17
|
+
* `check:boundaries` blocks either host-side call site from doing so.
|
|
18
18
|
*/
|
|
19
19
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, } from "./menu-service.js";
|
|
20
20
|
export { resolveForLocation, resolveMenuDoc, } from "./resolver.js";
|
|
@@ -48,13 +48,13 @@ export declare const DEFAULT_MAX_TREE_DEPTH = 5;
|
|
|
48
48
|
export declare const DEFAULT_MAX_ITEM_COUNT = 500;
|
|
49
49
|
/**
|
|
50
50
|
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
51
|
-
* `/…`, `http(s)://`, `mailto:`) as
|
|
51
|
+
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
52
52
|
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
|
|
53
53
|
* from this package's public surface (`navigation/index.ts` -> `@jini-ai/cms/navigation`) so the
|
|
54
|
-
* dependency runs the direction that is actually legal —
|
|
54
|
+
* dependency runs the direction that is actually legal — the host already depends on `@jini-ai/cms`, not
|
|
55
55
|
* the reverse — rather than each consumer hand-copying the predicate.
|
|
56
56
|
*
|
|
57
|
-
* The two
|
|
57
|
+
* The two host-side copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
|
|
58
58
|
* `value => passes ? value : "#"` wrapper around the identical predicate below) have **not yet been
|
|
59
59
|
* retired to import this** — that edit is intentionally out of this change's scope (a
|
|
60
60
|
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
@@ -79,7 +79,7 @@ export declare const DEFAULT_MAX_ITEM_COUNT = 500;
|
|
|
79
79
|
* workspace's stored menu data (`content.db`, `content.seed.db`) before this change: every existing
|
|
80
80
|
* `url`-kind href already starts with `#` or `/`, so this tightening rejects nothing that exists
|
|
81
81
|
* today — it governs `createMenu` too, so it also reaches the Jini MCP agent-tool path, not just
|
|
82
|
-
* hrefs written through
|
|
82
|
+
* hrefs written through the host's admin editor.
|
|
83
83
|
*
|
|
84
84
|
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
85
85
|
* otherwise a valid input — this function performs no other precondition checks.
|
|
@@ -67,13 +67,13 @@ const SAFE_HREF_RESOLUTION_BASE = "http://tovu-safehref.invalid/";
|
|
|
67
67
|
const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
68
68
|
/**
|
|
69
69
|
* The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
|
|
70
|
-
* `/…`, `http(s)://`, `mailto:`) as
|
|
70
|
+
* `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
|
|
71
71
|
* `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
|
|
72
72
|
* from this package's public surface (`navigation/index.ts` -> `@jini-ai/cms/navigation`) so the
|
|
73
|
-
* dependency runs the direction that is actually legal —
|
|
73
|
+
* dependency runs the direction that is actually legal — the host already depends on `@jini-ai/cms`, not
|
|
74
74
|
* the reverse — rather than each consumer hand-copying the predicate.
|
|
75
75
|
*
|
|
76
|
-
* The two
|
|
76
|
+
* The two host-side copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
|
|
77
77
|
* `value => passes ? value : "#"` wrapper around the identical predicate below) have **not yet been
|
|
78
78
|
* retired to import this** — that edit is intentionally out of this change's scope (a
|
|
79
79
|
* `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
|
|
@@ -98,7 +98,7 @@ const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
|
|
|
98
98
|
* workspace's stored menu data (`content.db`, `content.seed.db`) before this change: every existing
|
|
99
99
|
* `url`-kind href already starts with `#` or `/`, so this tightening rejects nothing that exists
|
|
100
100
|
* today — it governs `createMenu` too, so it also reaches the Jini MCP agent-tool path, not just
|
|
101
|
-
* hrefs written through
|
|
101
|
+
* hrefs written through the host's admin editor.
|
|
102
102
|
*
|
|
103
103
|
* @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
|
|
104
104
|
* otherwise a valid input — this function performs no other precondition checks.
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Relocated here from Jini's `@jini-ai/ui` (2026-08-08): these keys aren't UI-rendering logic,
|
|
6
6
|
* they're settings-domain presentation copy, and this package's `settings` module is the actual
|
|
7
|
-
* domain owner shared across every host
|
|
7
|
+
* domain owner shared across every host. `@jini-ai/ui` keeps the *component*
|
|
8
8
|
* that renders a settings dialog; a host supplies the copy, and this is the shared copy any
|
|
9
9
|
* OD-parity host can reuse instead of re-authoring its own. See project memory "Settings-dialog
|
|
10
|
-
* i18n relocation" for the full decision record and the sibling move of the 8
|
|
11
|
-
* capability-fact keys (those stayed host-side — see
|
|
10
|
+
* i18n relocation" for the full decision record and the sibling move of the 8 host-specific
|
|
11
|
+
* capability-fact keys (those stayed host-side — see the host's own `settings-capabilities-i18n.ts`).
|
|
12
12
|
*
|
|
13
13
|
* No `en` dictionary: this package's convention (matching `@jini-ai/ui`) is "the English string
|
|
14
14
|
* IS the key" — `translateSettingsDialog` falls back to the raw key for English and any
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Relocated here from Jini's `@jini-ai/ui` (2026-08-08): these keys aren't UI-rendering logic,
|
|
6
6
|
* they're settings-domain presentation copy, and this package's `settings` module is the actual
|
|
7
|
-
* domain owner shared across every host
|
|
7
|
+
* domain owner shared across every host. `@jini-ai/ui` keeps the *component*
|
|
8
8
|
* that renders a settings dialog; a host supplies the copy, and this is the shared copy any
|
|
9
9
|
* OD-parity host can reuse instead of re-authoring its own. See project memory "Settings-dialog
|
|
10
|
-
* i18n relocation" for the full decision record and the sibling move of the 8
|
|
11
|
-
* capability-fact keys (those stayed host-side — see
|
|
10
|
+
* i18n relocation" for the full decision record and the sibling move of the 8 host-specific
|
|
11
|
+
* capability-fact keys (those stayed host-side — see the host's own `settings-capabilities-i18n.ts`).
|
|
12
12
|
*
|
|
13
13
|
* No `en` dictionary: this package's convention (matching `@jini-ai/ui`) is "the English string
|
|
14
14
|
* IS the key" — `translateSettingsDialog` falls back to the raw key for English and any
|
|
@@ -26,18 +26,12 @@
|
|
|
26
26
|
* - `confirm()` structurally refuses an agent: `gateway.ts`'s own body — "if (principalKind ===
|
|
27
27
|
* 'agent') throw new ForbiddenError('agent principals may not confirm a gated mutation', ...)"
|
|
28
28
|
* — before any permission check even runs. No `taxonomy_confirm_merge_term`-equivalent tool
|
|
29
|
-
* exists in this catalog
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* kind='user'
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `confirmer-must-equal-own-delegatedBy` actor-class rule `database_execute_migrate_forward`/
|
|
36
|
-
* `backup_execute_restore` carry, which `assistant/tool-registration-kit.ts`'s
|
|
37
|
-
* `ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT` refuses to build regardless of this
|
|
38
|
-
* catalog's own choice, because the host's `ToolExecutor` has no confirmation transport wired.
|
|
39
|
-
* `taxonomy_execute_merge_term` is declared here (so the exclusion is a structural build-time
|
|
40
|
-
* refusal, not just an absent handler) but is NEVER wired by `tool-registrations.ts`.
|
|
29
|
+
* exists in this catalog.
|
|
30
|
+
* - `taxonomy_execute_merge_term` (2026-09-24) asks the human in chat before it runs: the host
|
|
31
|
+
* wires it with the kit's `humanConfirmedHandler`, which shows a confirm dialog and only on the
|
|
32
|
+
* human's own click confirms as that human (`kind='user'`) and executes as the agent acting for
|
|
33
|
+
* them — the `confirmer-must-equal-own-delegatedBy` rule. The model never sees or supplies a
|
|
34
|
+
* token. A merge deletes the source term's assignments with no Trash or undo, so it is gated.
|
|
41
35
|
*
|
|
42
36
|
* Naming: `taxonomy_*`, matching this package's own name — distinct from `features/content-types`'
|
|
43
37
|
* `collections_*` prefix (a different pairing) and from `features/entries`' own
|
|
@@ -48,9 +42,10 @@
|
|
|
48
42
|
* `ToolRegistration`s.
|
|
49
43
|
*
|
|
50
44
|
* Architectural role:
|
|
51
|
-
* `features/taxonomy` domain declaration.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
45
|
+
* `features/taxonomy` domain declaration. As of A2 (taxonomy plan) it imports nothing from its own
|
|
46
|
+
* `write-service.ts` — `contentType` is published as an open string (see `CONTENT_TYPE_SCHEMA`
|
|
47
|
+
* below), since Collections entries widened the set of legal values past the fixed `post`/`page`
|
|
48
|
+
* allow-list this file used to mirror as an `enum`.
|
|
54
49
|
*/
|
|
55
50
|
export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
|
|
56
51
|
export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
|
|
@@ -65,11 +60,11 @@ export interface AgentToolDefinition {
|
|
|
65
60
|
/**
|
|
66
61
|
* JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
|
|
67
62
|
* (`assistant/tool-registration-kit.ts`'s `buildDomainRegistrations`, which refuses to wire any
|
|
68
|
-
* tool lacking one).
|
|
63
|
+
* tool lacking one).
|
|
69
64
|
*/
|
|
70
65
|
inputSchema?: Readonly<Record<string, unknown>>;
|
|
71
66
|
}
|
|
72
|
-
/** The Taxonomy domain's fixed agent-tool catalog:
|
|
67
|
+
/** The Taxonomy domain's fixed agent-tool catalog: 7 wired (1 read, 5 ordinary writes, 1 gated-plan
|
|
73
68
|
* read) + 1 declared-but-never-wired destructive tool — see this file's header for the full
|
|
74
69
|
* `mergeTerm` safety analysis. */
|
|
75
70
|
export declare const taxonomyAgentToolCatalog: AgentToolDefinition[];
|
|
@@ -26,18 +26,12 @@
|
|
|
26
26
|
* - `confirm()` structurally refuses an agent: `gateway.ts`'s own body — "if (principalKind ===
|
|
27
27
|
* 'agent') throw new ForbiddenError('agent principals may not confirm a gated mutation', ...)"
|
|
28
28
|
* — before any permission check even runs. No `taxonomy_confirm_merge_term`-equivalent tool
|
|
29
|
-
* exists in this catalog
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* kind='user'
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `confirmer-must-equal-own-delegatedBy` actor-class rule `database_execute_migrate_forward`/
|
|
36
|
-
* `backup_execute_restore` carry, which `assistant/tool-registration-kit.ts`'s
|
|
37
|
-
* `ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT` refuses to build regardless of this
|
|
38
|
-
* catalog's own choice, because the host's `ToolExecutor` has no confirmation transport wired.
|
|
39
|
-
* `taxonomy_execute_merge_term` is declared here (so the exclusion is a structural build-time
|
|
40
|
-
* refusal, not just an absent handler) but is NEVER wired by `tool-registrations.ts`.
|
|
29
|
+
* exists in this catalog.
|
|
30
|
+
* - `taxonomy_execute_merge_term` (2026-09-24) asks the human in chat before it runs: the host
|
|
31
|
+
* wires it with the kit's `humanConfirmedHandler`, which shows a confirm dialog and only on the
|
|
32
|
+
* human's own click confirms as that human (`kind='user'`) and executes as the agent acting for
|
|
33
|
+
* them — the `confirmer-must-equal-own-delegatedBy` rule. The model never sees or supplies a
|
|
34
|
+
* token. A merge deletes the source term's assignments with no Trash or undo, so it is gated.
|
|
41
35
|
*
|
|
42
36
|
* Naming: `taxonomy_*`, matching this package's own name — distinct from `features/content-types`'
|
|
43
37
|
* `collections_*` prefix (a different pairing) and from `features/entries`' own
|
|
@@ -48,11 +42,11 @@
|
|
|
48
42
|
* `ToolRegistration`s.
|
|
49
43
|
*
|
|
50
44
|
* Architectural role:
|
|
51
|
-
* `features/taxonomy` domain declaration.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
45
|
+
* `features/taxonomy` domain declaration. As of A2 (taxonomy plan) it imports nothing from its own
|
|
46
|
+
* `write-service.ts` — `contentType` is published as an open string (see `CONTENT_TYPE_SCHEMA`
|
|
47
|
+
* below), since Collections entries widened the set of legal values past the fixed `post`/`page`
|
|
48
|
+
* allow-list this file used to mirror as an `enum`.
|
|
54
49
|
*/
|
|
55
|
-
import { TAXONOMY_ALLOWED_CONTENT_TYPES } from "./write-service.js";
|
|
56
50
|
const TAXONOMY_ID_SCHEMA = {
|
|
57
51
|
type: "string",
|
|
58
52
|
minLength: 1,
|
|
@@ -63,12 +57,17 @@ const TERM_ID_SCHEMA = {
|
|
|
63
57
|
minLength: 1,
|
|
64
58
|
description: "A term id, as returned by taxonomy_create_term or taxonomy_list.",
|
|
65
59
|
};
|
|
60
|
+
/** A2 (taxonomy plan) — an open string, not an `enum` of `TAXONOMY_ALLOWED_CONTENT_TYPES`. `post`
|
|
61
|
+
* and `page` are always eligible; a Collection key (e.g. `'recipes'`) is eligible when that
|
|
62
|
+
* Collection's own content-type policy allows it — a call the write-service's
|
|
63
|
+
* `contentTypeTaxonomyPolicy` makes, not this schema. An `enum` here would reject a valid
|
|
64
|
+
* Collection key before the request ever reached that check. */
|
|
66
65
|
const CONTENT_TYPE_SCHEMA = {
|
|
67
66
|
type: "string",
|
|
68
|
-
|
|
69
|
-
description: "
|
|
67
|
+
minLength: 1,
|
|
68
|
+
description: "'post', 'page', or a Collection key (entries of that Collection).",
|
|
70
69
|
};
|
|
71
|
-
/** The Taxonomy domain's fixed agent-tool catalog:
|
|
70
|
+
/** The Taxonomy domain's fixed agent-tool catalog: 7 wired (1 read, 5 ordinary writes, 1 gated-plan
|
|
72
71
|
* read) + 1 declared-but-never-wired destructive tool — see this file's header for the full
|
|
73
72
|
* `mergeTerm` safety analysis. */
|
|
74
73
|
export const taxonomyAgentToolCatalog = [
|
|
@@ -128,8 +127,8 @@ export const taxonomyAgentToolCatalog = [
|
|
|
128
127
|
{
|
|
129
128
|
name: "taxonomy_assign_terms",
|
|
130
129
|
description: "Assigns one or more existing terms to a piece of content (the same <TermPicker> operation the Collections editor and the Categories & " +
|
|
131
|
-
"Tags screen both use).
|
|
132
|
-
"
|
|
130
|
+
"Tags screen both use). This call ADDS assignments; calling it twice with the same term is a no-op (idempotent). Use " +
|
|
131
|
+
"taxonomy_unassign_terms to remove an assignment. Every termId is validated (must exist; its taxonomy must be applicable to contentType) " +
|
|
133
132
|
"before ANY row is written, so a bad id in the list rejects the whole call rather than partially assigning.",
|
|
134
133
|
sideEffects: "mutates-durable-state",
|
|
135
134
|
authorization: { permission: "admin.taxonomy.manage" },
|
|
@@ -139,19 +138,35 @@ export const taxonomyAgentToolCatalog = [
|
|
|
139
138
|
required: ["contentType", "contentId", "termIds"],
|
|
140
139
|
properties: {
|
|
141
140
|
contentType: CONTENT_TYPE_SCHEMA,
|
|
142
|
-
contentId: { type: "string", minLength: 1, description: "The id of the
|
|
141
|
+
contentId: { type: "string", minLength: 1, description: "The id of the content row to assign terms to." },
|
|
143
142
|
termIds: { type: "array", items: { type: "string" }, description: "Term ids to assign. May be empty (a no-op)." },
|
|
144
143
|
},
|
|
145
144
|
},
|
|
146
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: "taxonomy_unassign_terms",
|
|
148
|
+
description: "Removes one or more previously-assigned terms from a piece of content. Idempotent — removing a term that isn't currently assigned is a " +
|
|
149
|
+
"no-op, not an error. Every termId is validated the same way taxonomy_assign_terms validates them (must exist; its taxonomy must be " +
|
|
150
|
+
"applicable to contentType) before anything is removed.",
|
|
151
|
+
sideEffects: "mutates-durable-state",
|
|
152
|
+
authorization: { permission: "admin.taxonomy.manage" },
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
required: ["contentType", "contentId", "termIds"],
|
|
157
|
+
properties: {
|
|
158
|
+
contentType: CONTENT_TYPE_SCHEMA,
|
|
159
|
+
contentId: { type: "string", minLength: 1, description: "The id of the content row to unassign terms from." },
|
|
160
|
+
termIds: { type: "array", items: { type: "string" }, description: "Term ids to unassign. May be empty (a no-op)." },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
147
164
|
{
|
|
148
165
|
name: "taxonomy_plan_merge_term",
|
|
149
166
|
description: "Previews merging one term into another: recomputes and returns the current overlap-loss disclosure (how many pieces of content are " +
|
|
150
167
|
"already assigned to BOTH terms, whose duplicate assignment would be silently lost by the merge's own dedup step) plus a planId/planHash " +
|
|
151
168
|
"a human can use in the admin UI's own merge confirmation ceremony. Read-only — performs no merge. Rejects immediately if fromTermId " +
|
|
152
|
-
"equals intoTermId, before computing anything.
|
|
153
|
-
"through the admin UI — no tool in this catalog can do that step (by design: merges are destructive and irreversible for the merged-away " +
|
|
154
|
-
"term's own identity).",
|
|
169
|
+
"equals intoTermId, before computing anything. To actually merge, call taxonomy_execute_merge_term, which asks the user to confirm first.",
|
|
155
170
|
sideEffects: "none",
|
|
156
171
|
authorization: { permission: "admin.taxonomy.manage" },
|
|
157
172
|
inputSchema: {
|
|
@@ -165,13 +180,22 @@ export const taxonomyAgentToolCatalog = [
|
|
|
165
180
|
},
|
|
166
181
|
},
|
|
167
182
|
{
|
|
168
|
-
//
|
|
169
|
-
// own confirm() step structurally refuses an agent principal, and no confirmation transport
|
|
170
|
-
// exists for an agent to be handed a human-minted token through anyway.
|
|
183
|
+
// Asks the human first — see this file's header.
|
|
171
184
|
name: "taxonomy_execute_merge_term",
|
|
172
|
-
description: "
|
|
185
|
+
description: "Merges one term into another: every piece of content tagged with fromTermId is re-tagged with intoTermId, and fromTermId is " +
|
|
186
|
+
"deprecated. Shows the user a confirm dialog first and only merges if they confirm. The merge can't be undone. Call " +
|
|
187
|
+
"taxonomy_plan_merge_term first to see how many items are affected.",
|
|
173
188
|
sideEffects: "mutates-durable-state",
|
|
174
189
|
authorization: { permission: "admin.taxonomy.manage" },
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
additionalProperties: false,
|
|
193
|
+
required: ["fromTermId", "intoTermId"],
|
|
194
|
+
properties: {
|
|
195
|
+
fromTermId: { ...TERM_ID_SCHEMA, description: "The term to merge away. It is deprecated after the merge." },
|
|
196
|
+
intoTermId: { ...TERM_ID_SCHEMA, description: "The term that receives every assignment." },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
175
199
|
actorClassRule: "confirmer-must-equal-own-delegatedBy",
|
|
176
200
|
},
|
|
177
201
|
];
|