@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.
Files changed (306) hide show
  1. package/dist/content-types/index-provisioning.d.ts +1 -1
  2. package/dist/content-types/index-provisioning.js +1 -1
  3. package/dist/content-types/write-service.js +31 -11
  4. package/dist/core/entity-liveness.d.ts +55 -0
  5. package/dist/core/entity-liveness.js +38 -0
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/index.js +1 -0
  8. package/dist/core/ports.d.ts +1 -1
  9. package/dist/core/tools/registration-kit.d.ts +55 -17
  10. package/dist/core/tools/registration-kit.js +49 -20
  11. package/dist/identity/auth-service.d.ts +1 -1
  12. package/dist/identity/auth-service.js +1 -1
  13. package/dist/identity/seed.d.ts +1 -1
  14. package/dist/identity/seed.js +3 -3
  15. package/dist/media/agent-tools.d.ts +13 -3
  16. package/dist/media/agent-tools.js +45 -10
  17. package/dist/media/html-attributes.d.ts +6 -6
  18. package/dist/media/html-attributes.js +6 -6
  19. package/dist/media/index.d.ts +1 -1
  20. package/dist/media/index.js +1 -1
  21. package/dist/media/media-service.d.ts +2 -1
  22. package/dist/media/media-service.js +23 -7
  23. package/dist/media/tool-registrations.d.ts +12 -0
  24. package/dist/media/tool-registrations.js +20 -12
  25. package/dist/media/types.d.ts +1 -1
  26. package/dist/navigation/index.d.ts +2 -2
  27. package/dist/navigation/index.js +2 -2
  28. package/dist/navigation/menu-service.d.ts +4 -4
  29. package/dist/navigation/menu-service.js +4 -4
  30. package/dist/settings/dictionaries/index.d.ts +3 -3
  31. package/dist/settings/dictionaries/index.js +3 -3
  32. package/dist/taxonomy/agent-tools.d.ts +12 -17
  33. package/dist/taxonomy/agent-tools.js +53 -29
  34. package/dist/taxonomy/content-lookup.d.ts +35 -0
  35. package/dist/taxonomy/content-lookup.js +28 -0
  36. package/dist/taxonomy/index.d.ts +2 -2
  37. package/dist/taxonomy/index.js +2 -2
  38. package/dist/taxonomy/repo.memory.d.ts +11 -2
  39. package/dist/taxonomy/repo.memory.js +11 -0
  40. package/dist/taxonomy/write-service.d.ts +44 -15
  41. package/dist/taxonomy/write-service.js +60 -8
  42. package/package.json +3 -2
  43. package/dist/content-types/agent-tools.d.ts.map +0 -1
  44. package/dist/content-types/agent-tools.js.map +0 -1
  45. package/dist/content-types/cleanup.d.ts.map +0 -1
  46. package/dist/content-types/cleanup.js.map +0 -1
  47. package/dist/content-types/errors.d.ts.map +0 -1
  48. package/dist/content-types/errors.js.map +0 -1
  49. package/dist/content-types/field-defs.d.ts.map +0 -1
  50. package/dist/content-types/field-defs.js.map +0 -1
  51. package/dist/content-types/index-provisioning.d.ts.map +0 -1
  52. package/dist/content-types/index-provisioning.js.map +0 -1
  53. package/dist/content-types/index.d.ts.map +0 -1
  54. package/dist/content-types/index.js.map +0 -1
  55. package/dist/content-types/lifecycle-dispatch.d.ts.map +0 -1
  56. package/dist/content-types/lifecycle-dispatch.js.map +0 -1
  57. package/dist/content-types/lifecycle.d.ts.map +0 -1
  58. package/dist/content-types/lifecycle.js.map +0 -1
  59. package/dist/content-types/list.d.ts.map +0 -1
  60. package/dist/content-types/list.js.map +0 -1
  61. package/dist/content-types/repo.memory.d.ts.map +0 -1
  62. package/dist/content-types/repo.memory.js.map +0 -1
  63. package/dist/content-types/tool-registrations.d.ts.map +0 -1
  64. package/dist/content-types/tool-registrations.js.map +0 -1
  65. package/dist/content-types/types.d.ts.map +0 -1
  66. package/dist/content-types/types.js.map +0 -1
  67. package/dist/content-types/write-service.d.ts.map +0 -1
  68. package/dist/content-types/write-service.js.map +0 -1
  69. package/dist/core/commands/change-set.d.ts.map +0 -1
  70. package/dist/core/commands/change-set.js.map +0 -1
  71. package/dist/core/commands/command.d.ts.map +0 -1
  72. package/dist/core/commands/command.js.map +0 -1
  73. package/dist/core/index.d.ts.map +0 -1
  74. package/dist/core/index.js.map +0 -1
  75. package/dist/core/ports.d.ts.map +0 -1
  76. package/dist/core/ports.js.map +0 -1
  77. package/dist/core/tools/registration-kit.d.ts.map +0 -1
  78. package/dist/core/tools/registration-kit.js.map +0 -1
  79. package/dist/entries/agent-tools.d.ts.map +0 -1
  80. package/dist/entries/agent-tools.js.map +0 -1
  81. package/dist/entries/errors.d.ts.map +0 -1
  82. package/dist/entries/errors.js.map +0 -1
  83. package/dist/entries/field-validation.d.ts.map +0 -1
  84. package/dist/entries/field-validation.js.map +0 -1
  85. package/dist/entries/index.d.ts.map +0 -1
  86. package/dist/entries/index.js.map +0 -1
  87. package/dist/entries/lifecycle-dispatch.d.ts.map +0 -1
  88. package/dist/entries/lifecycle-dispatch.js.map +0 -1
  89. package/dist/entries/list.d.ts.map +0 -1
  90. package/dist/entries/list.js.map +0 -1
  91. package/dist/entries/repo.memory.d.ts.map +0 -1
  92. package/dist/entries/repo.memory.js.map +0 -1
  93. package/dist/entries/tool-registrations.d.ts.map +0 -1
  94. package/dist/entries/tool-registrations.js.map +0 -1
  95. package/dist/entries/types.d.ts.map +0 -1
  96. package/dist/entries/types.js.map +0 -1
  97. package/dist/entries/write-service.d.ts.map +0 -1
  98. package/dist/entries/write-service.js.map +0 -1
  99. package/dist/identity/admin-crud-service.d.ts.map +0 -1
  100. package/dist/identity/admin-crud-service.js.map +0 -1
  101. package/dist/identity/agent-tool-input.d.ts.map +0 -1
  102. package/dist/identity/agent-tool-input.js.map +0 -1
  103. package/dist/identity/agent-tools.d.ts.map +0 -1
  104. package/dist/identity/agent-tools.js.map +0 -1
  105. package/dist/identity/auth-service.d.ts.map +0 -1
  106. package/dist/identity/auth-service.js.map +0 -1
  107. package/dist/identity/authorize.d.ts.map +0 -1
  108. package/dist/identity/authorize.js.map +0 -1
  109. package/dist/identity/grant-service.d.ts.map +0 -1
  110. package/dist/identity/grant-service.js.map +0 -1
  111. package/dist/identity/hasher.d.ts.map +0 -1
  112. package/dist/identity/hasher.js.map +0 -1
  113. package/dist/identity/index.d.ts.map +0 -1
  114. package/dist/identity/index.js.map +0 -1
  115. package/dist/identity/password-policy.d.ts.map +0 -1
  116. package/dist/identity/password-policy.js.map +0 -1
  117. package/dist/identity/permission-migrations.d.ts.map +0 -1
  118. package/dist/identity/permission-migrations.js.map +0 -1
  119. package/dist/identity/permissions.d.ts.map +0 -1
  120. package/dist/identity/permissions.js.map +0 -1
  121. package/dist/identity/ports.d.ts.map +0 -1
  122. package/dist/identity/ports.js.map +0 -1
  123. package/dist/identity/repo.memory.d.ts.map +0 -1
  124. package/dist/identity/repo.memory.js.map +0 -1
  125. package/dist/identity/seed.d.ts.map +0 -1
  126. package/dist/identity/seed.js.map +0 -1
  127. package/dist/identity/tool-registrations.d.ts.map +0 -1
  128. package/dist/identity/tool-registrations.js.map +0 -1
  129. package/dist/identity/types.d.ts.map +0 -1
  130. package/dist/identity/types.js.map +0 -1
  131. package/dist/identity/username.d.ts.map +0 -1
  132. package/dist/identity/username.js.map +0 -1
  133. package/dist/media/agent-tools.d.ts.map +0 -1
  134. package/dist/media/agent-tools.js.map +0 -1
  135. package/dist/media/blob-gc-lock.d.ts.map +0 -1
  136. package/dist/media/blob-gc-lock.js.map +0 -1
  137. package/dist/media/blob-gc.d.ts.map +0 -1
  138. package/dist/media/blob-gc.js.map +0 -1
  139. package/dist/media/blob-key.d.ts.map +0 -1
  140. package/dist/media/blob-key.js.map +0 -1
  141. package/dist/media/blob-store.fs.d.ts.map +0 -1
  142. package/dist/media/blob-store.fs.js.map +0 -1
  143. package/dist/media/blob-store.memory.d.ts.map +0 -1
  144. package/dist/media/blob-store.memory.js.map +0 -1
  145. package/dist/media/content-type-sniffer.d.ts.map +0 -1
  146. package/dist/media/content-type-sniffer.js.map +0 -1
  147. package/dist/media/html-attributes.d.ts.map +0 -1
  148. package/dist/media/html-attributes.js.map +0 -1
  149. package/dist/media/image-transformer.d.ts.map +0 -1
  150. package/dist/media/image-transformer.js.map +0 -1
  151. package/dist/media/image-transformer.sharp.d.ts.map +0 -1
  152. package/dist/media/image-transformer.sharp.js.map +0 -1
  153. package/dist/media/index.d.ts.map +0 -1
  154. package/dist/media/index.js.map +0 -1
  155. package/dist/media/media-service.d.ts.map +0 -1
  156. package/dist/media/media-service.js.map +0 -1
  157. package/dist/media/ports.d.ts.map +0 -1
  158. package/dist/media/ports.js.map +0 -1
  159. package/dist/media/rendition-service.d.ts.map +0 -1
  160. package/dist/media/rendition-service.js.map +0 -1
  161. package/dist/media/repo.memory.d.ts.map +0 -1
  162. package/dist/media/repo.memory.js.map +0 -1
  163. package/dist/media/tool-registrations.d.ts.map +0 -1
  164. package/dist/media/tool-registrations.js.map +0 -1
  165. package/dist/media/transform-lock.d.ts.map +0 -1
  166. package/dist/media/transform-lock.js.map +0 -1
  167. package/dist/media/transform-registry.d.ts.map +0 -1
  168. package/dist/media/transform-registry.js.map +0 -1
  169. package/dist/media/transform-types.d.ts.map +0 -1
  170. package/dist/media/transform-types.js.map +0 -1
  171. package/dist/media/types.d.ts.map +0 -1
  172. package/dist/media/types.js.map +0 -1
  173. package/dist/navigation/agent-tools.d.ts.map +0 -1
  174. package/dist/navigation/agent-tools.js.map +0 -1
  175. package/dist/navigation/contracts.d.ts.map +0 -1
  176. package/dist/navigation/contracts.js.map +0 -1
  177. package/dist/navigation/index.d.ts.map +0 -1
  178. package/dist/navigation/index.js.map +0 -1
  179. package/dist/navigation/menu-service.d.ts.map +0 -1
  180. package/dist/navigation/menu-service.js.map +0 -1
  181. package/dist/navigation/ports.d.ts.map +0 -1
  182. package/dist/navigation/ports.js.map +0 -1
  183. package/dist/navigation/read-model.d.ts.map +0 -1
  184. package/dist/navigation/read-model.js.map +0 -1
  185. package/dist/navigation/reconcile.d.ts.map +0 -1
  186. package/dist/navigation/reconcile.js.map +0 -1
  187. package/dist/navigation/repo.memory.d.ts.map +0 -1
  188. package/dist/navigation/repo.memory.js.map +0 -1
  189. package/dist/navigation/resolver.d.ts.map +0 -1
  190. package/dist/navigation/resolver.js.map +0 -1
  191. package/dist/navigation/tool-registrations.d.ts.map +0 -1
  192. package/dist/navigation/tool-registrations.js.map +0 -1
  193. package/dist/navigation/types.d.ts.map +0 -1
  194. package/dist/navigation/types.js.map +0 -1
  195. package/dist/presentation/index.d.ts.map +0 -1
  196. package/dist/presentation/index.js.map +0 -1
  197. package/dist/presentation/presentation.d.ts.map +0 -1
  198. package/dist/presentation/presentation.js.map +0 -1
  199. package/dist/presentation/repo.memory.d.ts.map +0 -1
  200. package/dist/presentation/repo.memory.js.map +0 -1
  201. package/dist/server/index.d.ts.map +0 -1
  202. package/dist/server/index.js.map +0 -1
  203. package/dist/settings/agent-tools.d.ts.map +0 -1
  204. package/dist/settings/agent-tools.js.map +0 -1
  205. package/dist/settings/agent-writable-preferences.d.ts.map +0 -1
  206. package/dist/settings/agent-writable-preferences.js.map +0 -1
  207. package/dist/settings/change-feed.d.ts.map +0 -1
  208. package/dist/settings/change-feed.js.map +0 -1
  209. package/dist/settings/definitions-dispatch.d.ts.map +0 -1
  210. package/dist/settings/definitions-dispatch.js.map +0 -1
  211. package/dist/settings/dictionaries/index.d.ts.map +0 -1
  212. package/dist/settings/dictionaries/index.js.map +0 -1
  213. package/dist/settings/dictionaries/settings-dialog.ar.d.ts.map +0 -1
  214. package/dist/settings/dictionaries/settings-dialog.ar.js.map +0 -1
  215. package/dist/settings/dictionaries/settings-dialog.bn.d.ts.map +0 -1
  216. package/dist/settings/dictionaries/settings-dialog.bn.js.map +0 -1
  217. package/dist/settings/dictionaries/settings-dialog.de.d.ts.map +0 -1
  218. package/dist/settings/dictionaries/settings-dialog.de.js.map +0 -1
  219. package/dist/settings/dictionaries/settings-dialog.es.d.ts.map +0 -1
  220. package/dist/settings/dictionaries/settings-dialog.es.js.map +0 -1
  221. package/dist/settings/dictionaries/settings-dialog.fa.d.ts.map +0 -1
  222. package/dist/settings/dictionaries/settings-dialog.fa.js.map +0 -1
  223. package/dist/settings/dictionaries/settings-dialog.fr.d.ts.map +0 -1
  224. package/dist/settings/dictionaries/settings-dialog.fr.js.map +0 -1
  225. package/dist/settings/dictionaries/settings-dialog.hi.d.ts.map +0 -1
  226. package/dist/settings/dictionaries/settings-dialog.hi.js.map +0 -1
  227. package/dist/settings/dictionaries/settings-dialog.hu.d.ts.map +0 -1
  228. package/dist/settings/dictionaries/settings-dialog.hu.js.map +0 -1
  229. package/dist/settings/dictionaries/settings-dialog.id.d.ts.map +0 -1
  230. package/dist/settings/dictionaries/settings-dialog.id.js.map +0 -1
  231. package/dist/settings/dictionaries/settings-dialog.it.d.ts.map +0 -1
  232. package/dist/settings/dictionaries/settings-dialog.it.js.map +0 -1
  233. package/dist/settings/dictionaries/settings-dialog.ja.d.ts.map +0 -1
  234. package/dist/settings/dictionaries/settings-dialog.ja.js.map +0 -1
  235. package/dist/settings/dictionaries/settings-dialog.ko.d.ts.map +0 -1
  236. package/dist/settings/dictionaries/settings-dialog.ko.js.map +0 -1
  237. package/dist/settings/dictionaries/settings-dialog.pl.d.ts.map +0 -1
  238. package/dist/settings/dictionaries/settings-dialog.pl.js.map +0 -1
  239. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts.map +0 -1
  240. package/dist/settings/dictionaries/settings-dialog.pt-BR.js.map +0 -1
  241. package/dist/settings/dictionaries/settings-dialog.ru.d.ts.map +0 -1
  242. package/dist/settings/dictionaries/settings-dialog.ru.js.map +0 -1
  243. package/dist/settings/dictionaries/settings-dialog.th.d.ts.map +0 -1
  244. package/dist/settings/dictionaries/settings-dialog.th.js.map +0 -1
  245. package/dist/settings/dictionaries/settings-dialog.tr.d.ts.map +0 -1
  246. package/dist/settings/dictionaries/settings-dialog.tr.js.map +0 -1
  247. package/dist/settings/dictionaries/settings-dialog.uk.d.ts.map +0 -1
  248. package/dist/settings/dictionaries/settings-dialog.uk.js.map +0 -1
  249. package/dist/settings/dictionaries/settings-dialog.ur.d.ts.map +0 -1
  250. package/dist/settings/dictionaries/settings-dialog.ur.js.map +0 -1
  251. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts.map +0 -1
  252. package/dist/settings/dictionaries/settings-dialog.zh-CN.js.map +0 -1
  253. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts.map +0 -1
  254. package/dist/settings/dictionaries/settings-dialog.zh-TW.js.map +0 -1
  255. package/dist/settings/ensure-definitions.d.ts.map +0 -1
  256. package/dist/settings/ensure-definitions.js.map +0 -1
  257. package/dist/settings/errors.d.ts.map +0 -1
  258. package/dist/settings/errors.js.map +0 -1
  259. package/dist/settings/index.d.ts.map +0 -1
  260. package/dist/settings/index.js.map +0 -1
  261. package/dist/settings/ports.d.ts.map +0 -1
  262. package/dist/settings/ports.js.map +0 -1
  263. package/dist/settings/purge-service.d.ts.map +0 -1
  264. package/dist/settings/purge-service.js.map +0 -1
  265. package/dist/settings/repo.memory.d.ts.map +0 -1
  266. package/dist/settings/repo.memory.js.map +0 -1
  267. package/dist/settings/settings.d.ts.map +0 -1
  268. package/dist/settings/settings.js.map +0 -1
  269. package/dist/settings/tool-registrations.d.ts.map +0 -1
  270. package/dist/settings/tool-registrations.js.map +0 -1
  271. package/dist/settings/types.d.ts.map +0 -1
  272. package/dist/settings/types.js.map +0 -1
  273. package/dist/settings/ui-tab-definitions.d.ts.map +0 -1
  274. package/dist/settings/ui-tab-definitions.js.map +0 -1
  275. package/dist/settings/write-service.d.ts.map +0 -1
  276. package/dist/settings/write-service.js.map +0 -1
  277. package/dist/taxonomy/agent-tools.d.ts.map +0 -1
  278. package/dist/taxonomy/agent-tools.js.map +0 -1
  279. package/dist/taxonomy/content-lookup.d.ts.map +0 -1
  280. package/dist/taxonomy/content-lookup.js.map +0 -1
  281. package/dist/taxonomy/index.d.ts.map +0 -1
  282. package/dist/taxonomy/index.js.map +0 -1
  283. package/dist/taxonomy/list.d.ts.map +0 -1
  284. package/dist/taxonomy/list.js.map +0 -1
  285. package/dist/taxonomy/merge-term.d.ts.map +0 -1
  286. package/dist/taxonomy/merge-term.js.map +0 -1
  287. package/dist/taxonomy/repo.memory.d.ts.map +0 -1
  288. package/dist/taxonomy/repo.memory.js.map +0 -1
  289. package/dist/taxonomy/validation-chain.d.ts.map +0 -1
  290. package/dist/taxonomy/validation-chain.js.map +0 -1
  291. package/dist/taxonomy/write-service.d.ts.map +0 -1
  292. package/dist/taxonomy/write-service.js.map +0 -1
  293. package/dist/workspace/agent-tools.d.ts.map +0 -1
  294. package/dist/workspace/agent-tools.js.map +0 -1
  295. package/dist/workspace/create.d.ts.map +0 -1
  296. package/dist/workspace/create.js.map +0 -1
  297. package/dist/workspace/delete.d.ts.map +0 -1
  298. package/dist/workspace/delete.js.map +0 -1
  299. package/dist/workspace/index.d.ts.map +0 -1
  300. package/dist/workspace/index.js.map +0 -1
  301. package/dist/workspace/repo.memory.d.ts.map +0 -1
  302. package/dist/workspace/repo.memory.js.map +0 -1
  303. package/dist/workspace/tool-registrations.d.ts.map +0 -1
  304. package/dist/workspace/tool-registrations.js.map +0 -1
  305. package/dist/workspace/update.d.ts.map +0 -1
  306. 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 Tovu's admin
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 Tovu's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
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 (Tovu's `apps/admin`) risks pulling those into the admin
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 Tovu's `apps/website` render path, both pure
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 Tovu's admin
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 Tovu's admin copy (see this file's header).
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 Tovu's admin
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 Tovu's `FILE_HANDLER_ALLOWED_MIME_TYPES`/
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 (Tovu's `apps/admin`) risks pulling those into the admin
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 Tovu's `apps/website` render path, both pure
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 Tovu's admin
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 Tovu's admin copy (see this file's header).
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",
@@ -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";
@@ -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 Tovu's
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 Tovu's
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 Tovu, this file in `@jini-ai/cms`; this codebase's own
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. Tovu's `FILE_HANDLER_ALLOWED_MIME_TYPES`/`IMPORTABLE_CONTENT_TYPES`). Never throws and never
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 Tovu's `development/scripts/backfill-media-slugs.ts`).
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`, Tovu) calls this against arbitrary pre-existing
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}-byte size cap`);
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
- const { media } = await updateMediaMetadata({
125
- deps: { clock: routeDeps.clock, mediaRepo: routeDeps.mediaRepo },
126
- input: {
127
- workspaceId: routeDeps.workspaceId,
128
- id: mediaId,
129
- title: typeof input.title === "string" ? input.title : undefined,
130
- alt: typeof input.alt === "string" ? input.alt : undefined,
131
- caption: typeof input.caption === "string" ? input.caption : undefined,
132
- credit: typeof input.credit === "string" ? input.credit : undefined,
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");
@@ -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 Tovu's
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). Tovu's own render-time `safeHref` (`server/inbound/public-http/http/site/render.ts` and its
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 Tovu call site from doing so.
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";
@@ -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). Tovu's own render-time `safeHref` (`server/inbound/public-http/http/site/render.ts` and its
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 Tovu call site from doing so.
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 Tovu's render-time `safeHref` in `render.ts` and its
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 — Tovu already depends on `@jini-ai/cms`, not
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 Tovu copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
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 Tovu's admin editor.
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 Tovu's render-time `safeHref` in `render.ts` and its
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 — Tovu already depends on `@jini-ai/cms`, not
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 Tovu copies (`render.ts:252`, `features/theme/static-render.ts:210`, both a coercing
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 Tovu's admin editor.
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 (Tovu, Zana, ...). `@jini-ai/ui` keeps the *component*
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 Tovu-specific
11
- * capability-fact keys (those stayed host-side — see Tovu's own `settings-capabilities-i18n.ts`).
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 (Tovu, Zana, ...). `@jini-ai/ui` keeps the *component*
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 Tovu-specific
11
- * capability-fact keys (those stayed host-side — see Tovu's own `settings-capabilities-i18n.ts`).
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, and no description below may even imply that step (mirrors
30
- * `features/recovery/agent-tools.ts`'s own discipline for `backup_execute_restore`: "the
31
- * human confirmation step is exactly the one act the gated-mutations gateway reserves for
32
- * kind='user'/api_key principals").
33
- * - `execute()` needs a confirmation token a human already minted through the admin UI's own
34
- * ceremony (`server/routes/admin/taxonomy/merge-term.ts`'s `/merge/confirm` route) — the same
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. Imports only `TAXONOMY_ALLOWED_CONTENT_TYPES` from its
52
- * own `write-service.ts`, so the published `contentType` enum cannot drift from what the domain
53
- * actually accepts.
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). Optional — `taxonomy_execute_merge_term` is never wired, so it carries none.
63
+ * tool lacking one).
69
64
  */
70
65
  inputSchema?: Readonly<Record<string, unknown>>;
71
66
  }
72
- /** The Taxonomy domain's fixed agent-tool catalog: 6 wired (1 read, 4 ordinary writes, 1 gated-plan
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, and no description below may even imply that step (mirrors
30
- * `features/recovery/agent-tools.ts`'s own discipline for `backup_execute_restore`: "the
31
- * human confirmation step is exactly the one act the gated-mutations gateway reserves for
32
- * kind='user'/api_key principals").
33
- * - `execute()` needs a confirmation token a human already minted through the admin UI's own
34
- * ceremony (`server/routes/admin/taxonomy/merge-term.ts`'s `/merge/confirm` route) — the same
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. Imports only `TAXONOMY_ALLOWED_CONTENT_TYPES` from its
52
- * own `write-service.ts`, so the published `contentType` enum cannot drift from what the domain
53
- * actually accepts.
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
- enum: [...TAXONOMY_ALLOWED_CONTENT_TYPES],
69
- description: "The content kind the target row actually is. Only 'post' and 'page' are eligible for term assignment (a permanent allow-list) — any other value is rejected before anything is written.",
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: 6 wired (1 read, 4 ordinary writes, 1 gated-plan
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). Additive only — this call ADDS assignments; it never removes a term not present in termIds, and calling it twice " +
132
- "with the same term is a no-op (idempotent). Every termId is validated (must exist; its taxonomy must be applicable to contentType) " +
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 post/page row to assign terms to." },
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. This tool can only PLAN a merge; actually executing one requires a human to confirm " +
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
- // EXCLUDED BY DESIGN, never wired: see this file's header. Token-gated; `core/gated-mutations`'s
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: "Executes a previously confirmed term merge using a human-minted confirmation token. NEVER agent-callable — see file header.",
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
  ];