@jini-ai/cms 0.3.11 → 0.4.1

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 (262) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +47 -19
  3. package/dist/content-types/agent-tools.d.ts +3 -56
  4. package/dist/content-types/agent-tools.js +22 -6
  5. package/dist/content-types/cleanup.d.ts +18 -13
  6. package/dist/content-types/cleanup.js +16 -11
  7. package/dist/content-types/errors.d.ts +57 -24
  8. package/dist/content-types/errors.js +56 -39
  9. package/dist/content-types/field-defs.d.ts +8 -4
  10. package/dist/content-types/field-defs.js +19 -14
  11. package/dist/content-types/index-provisioning.d.ts +61 -47
  12. package/dist/content-types/index-provisioning.js +90 -66
  13. package/dist/content-types/index.d.ts +4 -3
  14. package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
  15. package/dist/content-types/lifecycle-dispatch.js +5 -4
  16. package/dist/content-types/lifecycle.d.ts +23 -18
  17. package/dist/content-types/lifecycle.js +71 -64
  18. package/dist/content-types/repo.memory.d.ts +5 -4
  19. package/dist/content-types/repo.memory.js +3 -2
  20. package/dist/content-types/tool-registrations.d.ts +6 -8
  21. package/dist/content-types/tool-registrations.js +24 -28
  22. package/dist/content-types/types.d.ts +17 -13
  23. package/dist/content-types/types.js +11 -5
  24. package/dist/content-types/write-service.d.ts +20 -22
  25. package/dist/content-types/write-service.js +78 -75
  26. package/dist/core/authorization.d.ts +15 -0
  27. package/dist/core/authorization.js +2 -0
  28. package/dist/core/commands/change-set.d.ts +15 -7
  29. package/dist/core/commands/command.d.ts +45 -26
  30. package/dist/core/commands/command.js +73 -63
  31. package/dist/core/entity-liveness.d.ts +11 -7
  32. package/dist/core/entity-liveness.js +5 -4
  33. package/dist/core/index.d.ts +12 -5
  34. package/dist/core/index.js +10 -4
  35. package/dist/core/ports.d.ts +25 -24
  36. package/dist/core/ports.js +1 -0
  37. package/dist/core/tools/args.d.ts +49 -0
  38. package/dist/core/tools/args.js +46 -0
  39. package/dist/core/tools/index.d.ts +3 -0
  40. package/dist/core/tools/index.js +3 -0
  41. package/dist/entries/agent-tools.d.ts +1 -73
  42. package/dist/entries/agent-tools.js +19 -13
  43. package/dist/entries/errors.d.ts +28 -14
  44. package/dist/entries/errors.js +17 -11
  45. package/dist/entries/field-validation.d.ts +5 -3
  46. package/dist/entries/field-validation.js +7 -5
  47. package/dist/entries/index.d.ts +2 -2
  48. package/dist/entries/lifecycle-dispatch.d.ts +5 -2
  49. package/dist/entries/lifecycle-dispatch.js +2 -1
  50. package/dist/entries/list.d.ts +4 -2
  51. package/dist/entries/list.js +4 -3
  52. package/dist/entries/repo.memory.d.ts +12 -10
  53. package/dist/entries/repo.memory.js +11 -10
  54. package/dist/entries/tool-registrations.d.ts +6 -8
  55. package/dist/entries/tool-registrations.js +42 -41
  56. package/dist/entries/types.d.ts +0 -7
  57. package/dist/entries/write-service.d.ts +28 -29
  58. package/dist/entries/write-service.js +114 -105
  59. package/dist/http/settings/cms-adapter.d.ts +20 -0
  60. package/dist/http/settings/cms-adapter.js +124 -0
  61. package/dist/http/settings/contracts.d.ts +135 -0
  62. package/dist/http/settings/contracts.js +2 -0
  63. package/dist/http/settings/http.d.ts +10 -0
  64. package/dist/http/settings/http.js +211 -0
  65. package/dist/http/settings/index.d.ts +5 -0
  66. package/dist/http/settings/index.js +4 -0
  67. package/dist/http/settings/shared.d.ts +68 -0
  68. package/dist/http/settings/shared.js +54 -0
  69. package/dist/http/settings/stream.d.ts +24 -0
  70. package/dist/http/settings/stream.js +147 -0
  71. package/dist/media/agent-tools.d.ts +1 -66
  72. package/dist/media/agent-tools.js +5 -1
  73. package/dist/media/blob-gc-lock.d.ts +6 -2
  74. package/dist/media/blob-gc-lock.js +4 -2
  75. package/dist/media/blob-gc.d.ts +64 -51
  76. package/dist/media/blob-gc.js +62 -52
  77. package/dist/media/blob-key.d.ts +1 -1
  78. package/dist/media/content-type-sniffer.d.ts +3 -1
  79. package/dist/media/content-type-sniffer.js +1 -1
  80. package/dist/media/html-attributes.d.ts +9 -5
  81. package/dist/media/html-attributes.js +8 -6
  82. package/dist/media/image-transformer.js +1 -1
  83. package/dist/media/image-transformer.sharp.d.ts +6 -0
  84. package/dist/media/image-transformer.sharp.js +12 -6
  85. package/dist/media/import/fetch-image.d.ts +24 -0
  86. package/dist/media/import/fetch-image.js +137 -0
  87. package/dist/media/import/index.d.ts +3 -0
  88. package/dist/media/import/index.js +3 -0
  89. package/dist/media/import/ports.d.ts +38 -0
  90. package/dist/media/index.d.ts +7 -1
  91. package/dist/media/index.js +1 -0
  92. package/dist/media/media-service.d.ts +14 -10
  93. package/dist/media/media-service.js +57 -52
  94. package/dist/media/ports.d.ts +3 -2
  95. package/dist/media/rendition-service.d.ts +3 -3
  96. package/dist/media/rendition-service.js +38 -37
  97. package/dist/media/repo.memory.d.ts +18 -7
  98. package/dist/media/repo.memory.js +12 -6
  99. package/dist/media/tool-registrations.d.ts +16 -15
  100. package/dist/media/tool-registrations.js +84 -72
  101. package/dist/media/transform-lock.d.ts +22 -12
  102. package/dist/media/transform-lock.js +14 -13
  103. package/dist/media/transform-registry.d.ts +3 -3
  104. package/dist/media/transform-registry.js +19 -18
  105. package/dist/media/transform-types.d.ts +8 -3
  106. package/dist/media/transform-types.js +10 -5
  107. package/dist/media/types.d.ts +21 -4
  108. package/dist/media/types.js +15 -2
  109. package/dist/navigation/agent-tools.d.ts +1 -80
  110. package/dist/navigation/agent-tools.js +15 -3
  111. package/dist/navigation/contracts.d.ts +1 -1
  112. package/dist/navigation/index.d.ts +2 -2
  113. package/dist/navigation/menu-service.d.ts +57 -40
  114. package/dist/navigation/menu-service.js +80 -69
  115. package/dist/navigation/ports.d.ts +1 -1
  116. package/dist/navigation/read-model.d.ts +3 -1
  117. package/dist/navigation/read-model.js +2 -2
  118. package/dist/navigation/reconcile.d.ts +2 -2
  119. package/dist/navigation/reconcile.js +2 -1
  120. package/dist/navigation/repo.memory.d.ts +7 -3
  121. package/dist/navigation/repo.memory.js +4 -2
  122. package/dist/navigation/resolver.d.ts +4 -1
  123. package/dist/navigation/resolver.js +1 -1
  124. package/dist/navigation/tool-registrations.d.ts +6 -8
  125. package/dist/navigation/tool-registrations.js +44 -42
  126. package/dist/navigation/types.d.ts +1 -1
  127. package/dist/presentation/presentation.d.ts +11 -3
  128. package/dist/presentation/presentation.js +13 -6
  129. package/dist/presentation/repo.memory.d.ts +6 -2
  130. package/dist/presentation/repo.memory.js +3 -2
  131. package/dist/settings/agent-tools.d.ts +2 -75
  132. package/dist/settings/agent-tools.js +49 -45
  133. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  134. package/dist/settings/agent-value-write-tools.js +154 -0
  135. package/dist/settings/agent-writable-preferences.js +8 -20
  136. package/dist/settings/agent-write-denylist.d.ts +19 -0
  137. package/dist/settings/agent-write-denylist.js +15 -0
  138. package/dist/settings/change-feed.d.ts +11 -2
  139. package/dist/settings/change-feed.js +4 -4
  140. package/dist/settings/definitions-dispatch.d.ts +8 -3
  141. package/dist/settings/definitions-dispatch.js +5 -5
  142. package/dist/settings/dictionaries/index.d.ts +5 -2
  143. package/dist/settings/dictionaries/index.js +2 -2
  144. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
  145. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
  146. package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
  147. package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
  148. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
  149. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
  150. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
  151. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
  152. package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
  153. package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
  154. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
  155. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
  156. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
  157. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
  158. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
  159. package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
  160. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
  161. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
  162. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
  163. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
  164. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
  165. package/dist/settings/ensure-definitions.d.ts +5 -5
  166. package/dist/settings/errors.d.ts +7 -1
  167. package/dist/settings/errors.js +7 -1
  168. package/dist/settings/index.d.ts +4 -1
  169. package/dist/settings/index.js +1 -0
  170. package/dist/settings/ports.d.ts +6 -5
  171. package/dist/settings/principal-lookup.d.ts +9 -0
  172. package/dist/settings/principal-lookup.js +2 -0
  173. package/dist/settings/purge-service.d.ts +5 -4
  174. package/dist/settings/purge-service.js +3 -2
  175. package/dist/settings/settings.d.ts +22 -7
  176. package/dist/settings/settings.js +47 -26
  177. package/dist/settings/tool-registrations.d.ts +27 -32
  178. package/dist/settings/tool-registrations.js +66 -56
  179. package/dist/settings/types.d.ts +1 -1
  180. package/dist/settings/ui-tab-definitions.d.ts +1 -1
  181. package/dist/settings/write-service.d.ts +32 -26
  182. package/dist/settings/write-service.js +26 -20
  183. package/dist/taxonomy/agent-tools.d.ts +1 -66
  184. package/dist/taxonomy/agent-tools.js +25 -15
  185. package/dist/taxonomy/content-lookup.js +1 -1
  186. package/dist/taxonomy/index.d.ts +2 -2
  187. package/dist/taxonomy/merge-term.d.ts +7 -4
  188. package/dist/taxonomy/merge-term.js +7 -5
  189. package/dist/taxonomy/repo.memory.d.ts +42 -19
  190. package/dist/taxonomy/repo.memory.js +14 -12
  191. package/dist/taxonomy/validation-chain.d.ts +39 -18
  192. package/dist/taxonomy/validation-chain.js +35 -25
  193. package/dist/taxonomy/write-service.d.ts +103 -44
  194. package/dist/taxonomy/write-service.js +221 -179
  195. package/dist/trash/adapters/record-store.d.ts +52 -0
  196. package/dist/trash/adapters/record-store.js +36 -0
  197. package/dist/trash/cursor.d.ts +20 -0
  198. package/dist/trash/cursor.js +36 -0
  199. package/dist/trash/follow-ups.d.ts +76 -0
  200. package/dist/trash/follow-ups.js +46 -0
  201. package/dist/trash/index.d.ts +8 -0
  202. package/dist/trash/index.js +8 -0
  203. package/dist/trash/ports.d.ts +308 -0
  204. package/dist/trash/ports.js +2 -0
  205. package/dist/trash/repo.memory.d.ts +50 -0
  206. package/dist/trash/repo.memory.js +114 -0
  207. package/dist/trash/sweeper.d.ts +77 -0
  208. package/dist/trash/sweeper.js +127 -0
  209. package/dist/trash/write-service.d.ts +100 -0
  210. package/dist/trash/write-service.js +242 -0
  211. package/dist/workspace/agent-tools.d.ts +1 -59
  212. package/dist/workspace/agent-tools.js +27 -12
  213. package/dist/workspace/create.d.ts +31 -6
  214. package/dist/workspace/create.js +18 -5
  215. package/dist/workspace/delete.d.ts +18 -13
  216. package/dist/workspace/delete.js +20 -12
  217. package/dist/workspace/index.d.ts +1 -1
  218. package/dist/workspace/repo.memory.d.ts +19 -4
  219. package/dist/workspace/repo.memory.js +26 -4
  220. package/dist/workspace/tool-registrations.d.ts +2 -2
  221. package/dist/workspace/tool-registrations.js +13 -13
  222. package/dist/workspace/update.d.ts +6 -5
  223. package/dist/workspace/update.js +9 -8
  224. package/package.json +44 -38
  225. package/dist/core/tools/registration-kit.d.ts +0 -346
  226. package/dist/core/tools/registration-kit.js +0 -426
  227. package/dist/identity/admin-crud-service.d.ts +0 -280
  228. package/dist/identity/admin-crud-service.js +0 -468
  229. package/dist/identity/agent-tool-input.d.ts +0 -58
  230. package/dist/identity/agent-tool-input.js +0 -126
  231. package/dist/identity/agent-tools.d.ts +0 -101
  232. package/dist/identity/agent-tools.js +0 -290
  233. package/dist/identity/auth-service.d.ts +0 -118
  234. package/dist/identity/auth-service.js +0 -170
  235. package/dist/identity/authorize.d.ts +0 -90
  236. package/dist/identity/authorize.js +0 -94
  237. package/dist/identity/grant-service.d.ts +0 -198
  238. package/dist/identity/grant-service.js +0 -361
  239. package/dist/identity/hasher.d.ts +0 -39
  240. package/dist/identity/hasher.js +0 -39
  241. package/dist/identity/index.d.ts +0 -62
  242. package/dist/identity/index.js +0 -54
  243. package/dist/identity/password-policy.d.ts +0 -57
  244. package/dist/identity/password-policy.js +0 -66
  245. package/dist/identity/permission-migrations.d.ts +0 -86
  246. package/dist/identity/permission-migrations.js +0 -67
  247. package/dist/identity/permissions.d.ts +0 -35
  248. package/dist/identity/permissions.js +0 -603
  249. package/dist/identity/ports.d.ts +0 -201
  250. package/dist/identity/repo.memory.d.ts +0 -164
  251. package/dist/identity/repo.memory.js +0 -224
  252. package/dist/identity/seed.d.ts +0 -54
  253. package/dist/identity/seed.js +0 -429
  254. package/dist/identity/tool-registrations.d.ts +0 -50
  255. package/dist/identity/tool-registrations.js +0 -419
  256. package/dist/identity/types.d.ts +0 -185
  257. package/dist/identity/types.js +0 -64
  258. package/dist/identity/username.d.ts +0 -12
  259. package/dist/identity/username.js +0 -14
  260. package/dist/server/index.d.ts +0 -28
  261. package/dist/server/index.js +0 -28
  262. /package/dist/{identity → media/import}/ports.js +0 -0
@@ -1,3 +1,4 @@
1
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
1
2
  /**
2
3
  * @file Write-service for the `navigation` library.
3
4
  *
@@ -9,14 +10,14 @@
9
10
  *
10
11
  * How it relates to the project:
11
12
  * - Storage is `MenuRepoPort` (`repo.memory.ts`) — a self-contained repo, not
12
- * the generic entries repo (see `repo.memory.ts`'s file header for why).
13
+ * the generic entries repo (see `repo.memory.ts`'s file header for why).
13
14
  * - Location assignment additionally writes `NavLocationBindingRepoPort`
14
- * (`ports.ts`), the one real port this library declares.
15
+ * (`ports.ts`), the one real port this library declares.
15
16
  * - In production this whole module runs inside the command
16
- * gateway/write chokepoint (same-transaction revision, attribution,
17
- * `entry_refs` extraction). That gateway is not implemented as running code
18
- * yet, so these functions are the chokepoint's future *contents*, called
19
- * directly for now.
17
+ * gateway/write chokepoint (same-transaction revision, attribution,
18
+ * `entry_refs` extraction). That gateway is not implemented as running code
19
+ * yet, so these functions are the chokepoint's future *contents* called
20
+ * directly for now.
20
21
  *
21
22
  * Architectural role:
22
23
  * Feature logic only. No Express/route code, no direct SQL — everything goes
@@ -28,7 +29,7 @@ import { NAV_DOC_TYPE, } from "./types.js";
28
29
  // Outbox event publication — each mutating function below
29
30
  // enqueues its matching NAVIGATION_EVENTS entry after its repo write(s)
30
31
  // succeed, never on a rejection path. Mirrors the host's proven
31
- // `outbox.enqueue()` call shape.
32
+ // `outbox.enqueue` call shape.
32
33
  // ---------------------------------------------------------------------------
33
34
  /**
34
35
  * `payload` is typed as a plain `Record<string, unknown>` (not the specific
@@ -42,7 +43,7 @@ function buildEvent(required) {
42
43
  return {
43
44
  id: required.idGen.newId(),
44
45
  name: required.name,
45
- occurredAt: required.clock.nowIso(),
46
+ occurredAt: kernelNowIso({ clock: required.clock }),
46
47
  aggregateId: required.aggregateId,
47
48
  workspaceId: required.workspaceId,
48
49
  payload: required.payload,
@@ -52,10 +53,19 @@ function buildEvent(required) {
52
53
  // Typed errors
53
54
  // ---------------------------------------------------------------------------
54
55
  export class MenuNotFoundError extends Error {
56
+ constructor({ message }, optionalArgs = {}) {
57
+ super(message, optionalArgs);
58
+ }
55
59
  }
56
60
  export class MenuValidationError extends Error {
61
+ constructor({ message }, optionalArgs = {}) {
62
+ super(message, optionalArgs);
63
+ }
57
64
  }
58
65
  export class MenuConflictError extends Error {
66
+ constructor({ message }, optionalArgs = {}) {
67
+ super(message, optionalArgs);
68
+ }
59
69
  }
60
70
  /**
61
71
  * The 409-style purge rejection: hard delete is blocked while the menu is
@@ -65,8 +75,9 @@ export class MenuConflictError extends Error {
65
75
  */
66
76
  export class MenuLocationBoundError extends MenuConflictError {
67
77
  boundLocations;
68
- constructor(message, boundLocations) {
69
- super(message);
78
+ constructor(requiredArgs, optionalArgs = {}) {
79
+ const { message, boundLocations } = requiredArgs;
80
+ super({ message: message });
70
81
  this.boundLocations = boundLocations;
71
82
  }
72
83
  }
@@ -83,17 +94,17 @@ const RESERVED_TARGET_KINDS = new Set(["dynamicQuery", "content"]);
83
94
  /**
84
95
  * Fixed placeholder origin {@link isAllowedHref} resolves a claimed same-origin-relative href
85
96
  * against, to compare the RESOLVED origin rather than the raw string shape. Mirrors
86
- * `apps/website/src/server/inbound/public-http/http/site/render.ts`'s identically-named constant
97
+ * the host's public renderer's identically-named constant
87
98
  * byte-for-byte — see that file's own header for why a fixed placeholder origin is sufficient (only
88
99
  * the relationship between the resolved URL's origin and this one is ever inspected, never the
89
100
  * placeholder value itself).
90
101
  */
91
- const SAFE_HREF_RESOLUTION_BASE = "http://tovu-safehref.invalid/";
102
+ const SAFE_HREF_RESOLUTION_BASE = "http://safehref.invalid/";
92
103
  const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
93
104
  /**
94
105
  * The CANONICAL href allowlist for an author-authored link — same accepted shapes (`#…`, same-origin
95
106
  * `/…`, `http(s)://`, `mailto:`) as the host's render-time `safeHref` in `render.ts` and its
96
- * `features/theme/static-render.ts` duplicate. **This is now the single source of truth**: exported
107
+ * `features/theme/static-render.ts` duplicate. **This is now the single source of truth** exported
97
108
  * from this package's public surface (`navigation/index.ts` -> `@jini-ai/cms/navigation`) so the
98
109
  * dependency runs the direction that is actually legal — the host already depends on `@jini-ai/cms`, not
99
110
  * the reverse — rather than each consumer hand-copying the predicate.
@@ -104,14 +115,14 @@ const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
104
115
  * `check:boundaries` no-deep-import concern is NOT what blocks it: `render.ts` already imports
105
116
  * `@jini-ai/cms/core` today, and `features/theme` already imports `@jini-ai/cms/core` too, so both
106
117
  * call sites may legally import `@jini-ai/cms/navigation` directly — verified by grep, not assumed).
107
- * Until that follow-up lands, `apps/website/development/scripts/check-menu-href-allowlist-sync.ts`
118
+ * Until that follow-up lands, the host's menu href allowlist sync check
108
119
  * behaviorally gates the three copies against the same adversarial table so a divergence fails CI
109
120
  * instead of drifting silently.
110
121
  *
111
- * Replaced (this change) a `URL_SCHEME_DENYLIST` (`startsWith` on a lowercased, `.trim()`ed string)
122
+ * Replaced (this change) a `URL_SCHEME_DENYLIST` (`startsWith` on a lowercased, `.trim`ed string)
112
123
  * that failed open: the WHATWG `URL` parser strips TAB/LF/CR from ANYWHERE in the input and folds a
113
- * leading backslash to `/` for `http(s)`, so `"java\tscript:alert(1)"`, `" javascript:alert(1)"`,
114
- * `"file:///etc/passwd"`, `"blob:…"`, `"about:blank"`, `"//evil.example"` (protocol-relative), and
124
+ * leading backslash to `/` for `http(s)`, so `"java\tscript:alert"`, `" javascript:alert"`,
125
+ * `"file:// etc/passwd"`, `"blob:…"`, `"about:blank"`, `"//evil.example"` (protocol-relative), and
115
126
  * `"/\evil.example"` all resolved to a dangerous or off-origin target while never matching any
116
127
  * `startsWith` prefix in the old list. An allowlist closes the whole bypass class at once (case,
117
128
  * embedded control characters, and scheme are all irrelevant to an allowlist the same way) rather
@@ -126,11 +137,11 @@ const SAFE_HREF_RESOLUTION_ORIGIN = new URL(SAFE_HREF_RESOLUTION_BASE).origin;
126
137
  * hrefs written through the host's admin editor.
127
138
  *
128
139
  * @param rawHref - The href to check, already confirmed non-empty by `validateTarget`. Any string is
129
- * otherwise a valid input — this function performs no other precondition checks.
140
+ * otherwise a valid input — this function performs no other precondition checks.
130
141
  * @returns `true` when `rawHref` passes the allowlist, `false` otherwise.
131
- * @complexity O(1) — a handful of string checks plus one `URL` construction on the `/…` branch.
142
+ * @complexity O — a handful of string checks plus one `URL` construction on the `/…` branch.
132
143
  */
133
- export function isAllowedHref(rawHref) {
144
+ export function isAllowedHref({ rawHref }, _optional = {}) {
134
145
  const href = rawHref.trim();
135
146
  if (href.startsWith("#"))
136
147
  return true;
@@ -159,51 +170,51 @@ export const ALLOWED_HREF_SHAPES_DESCRIPTION = "an in-page anchor ('#...'), a sa
159
170
  *
160
171
  * Checks (total/bounded validation + target integrity):
161
172
  * - `items` (and every `children` array) is actually an array, and every node
162
- * in it is a non-null object — untrusted JSON bodies can carry `null`
163
- * entries or a non-array `items`/`children`, which must reject as a
164
- * `MenuValidationError`, not throw an uncaught `TypeError`;
173
+ * in it is a non-null object — untrusted JSON bodies can carry `null`
174
+ * entries or a non-array `items`/`children`, which must reject as a
175
+ * `MenuValidationError`, not throw an uncaught `TypeError`;
165
176
  * - every node has a non-empty `id`, unique across the whole tree (id
166
- * stability is a caller responsibility — see `updateMenuTree` doc for the
167
- * simplification this build accepts);
177
+ * stability is a caller responsibility — see `updateMenuTree` doc for the
178
+ * simplification this build accepts);
168
179
  * - nesting depth stays within `maxDepth`;
169
180
  * - total item count stays within `maxItemCount`;
170
181
  * - every node has a `target` object (a missing/`null` `target` rejects
171
- * rather than crashing) whose `kind` is one of the four v1 kinds — reserved
172
- * kinds (`dynamicQuery`, `content`) are rejected with a clear "not yet"
173
- * error rather than silently accepted;
182
+ * rather than crashing) whose `kind` is one of the four v1 kinds — reserved
183
+ * kinds (`dynamicQuery`, `content`) are rejected with a clear "not yet"
184
+ * error rather than silently accepted;
174
185
  * - `url` targets require a non-empty string `href` and pass an origin/scheme
175
- * ALLOWLIST (`#…`, same-origin `/…`, `http(s)://`, `mailto:` — see
176
- * {@link isAllowedHref}), not a scheme denylist.
186
+ * ALLOWLIST (`#…`, same-origin `/…`, `http(s)://`, `mailto:` — see
187
+ * {@link isAllowedHref}), not a scheme denylist.
177
188
  *
178
189
  * @complexity O(n) over total node count for one full walk; no per-node
179
190
  * backtracking. Space is O(n) for the cloned tree plus O(n) for the id set.
180
191
  * @overallScore 100
181
192
  */
182
- export function validateAndCloneTree(items, limits = {}) {
193
+ export function validateAndCloneTree({ items }, limits = {}) {
183
194
  const maxDepth = limits.maxDepth ?? DEFAULT_MAX_TREE_DEPTH;
184
195
  const maxItemCount = limits.maxItemCount ?? DEFAULT_MAX_ITEM_COUNT;
185
196
  const seenIds = new Set();
186
197
  let count = 0;
187
198
  function walk(nodes, depth) {
188
199
  if (!Array.isArray(nodes)) {
189
- throw new MenuValidationError("menu items must be an array");
200
+ throw new MenuValidationError({ message: "menu items must be an array" });
190
201
  }
191
202
  if (depth > maxDepth) {
192
- throw new MenuValidationError(`menu tree exceeds max nesting depth of ${maxDepth}`);
203
+ throw new MenuValidationError({ message: `menu tree exceeds max nesting depth of ${maxDepth}` });
193
204
  }
194
205
  return nodes.map((node) => {
195
206
  count += 1;
196
207
  if (count > maxItemCount) {
197
- throw new MenuValidationError(`menu tree exceeds max item count of ${maxItemCount}`);
208
+ throw new MenuValidationError({ message: `menu tree exceeds max item count of ${maxItemCount}` });
198
209
  }
199
210
  if (!node || typeof node !== "object") {
200
- throw new MenuValidationError("every menu item must be an object");
211
+ throw new MenuValidationError({ message: "every menu item must be an object" });
201
212
  }
202
213
  if (!node.id || !node.id.trim()) {
203
- throw new MenuValidationError("every menu item requires a non-empty id");
214
+ throw new MenuValidationError({ message: "every menu item requires a non-empty id" });
204
215
  }
205
216
  if (seenIds.has(node.id)) {
206
- throw new MenuValidationError(`duplicate item id '${node.id}' in menu tree`);
217
+ throw new MenuValidationError({ message: `duplicate item id '${node.id}' in menu tree` });
207
218
  }
208
219
  seenIds.add(node.id);
209
220
  validateTarget(node.target);
@@ -221,30 +232,30 @@ export function validateAndCloneTree(items, limits = {}) {
221
232
  */
222
233
  function validateTarget(target) {
223
234
  if (!target || typeof target !== "object") {
224
- throw new MenuValidationError("every menu item requires a target");
235
+ throw new MenuValidationError({ message: "every menu item requires a target" });
225
236
  }
226
237
  const kind = target.kind;
227
238
  if (RESERVED_TARGET_KINDS.has(kind)) {
228
- throw new MenuValidationError(`target kind '${kind}' is a reserved seam and is not supported yet`);
239
+ throw new MenuValidationError({ message: `target kind '${kind}' is a reserved seam and is not supported yet` });
229
240
  }
230
241
  if (!VALID_TARGET_KINDS.has(kind)) {
231
- throw new MenuValidationError(`unknown target kind '${kind}'`);
242
+ throw new MenuValidationError({ message: `unknown target kind '${kind}'` });
232
243
  }
233
244
  if (kind === "url") {
234
245
  const rawHref = target.href;
235
246
  if (typeof rawHref !== "string" || !rawHref.trim()) {
236
- throw new MenuValidationError("url target requires a non-empty href");
247
+ throw new MenuValidationError({ message: "url target requires a non-empty href" });
237
248
  }
238
- if (!isAllowedHref(rawHref)) {
239
- throw new MenuValidationError(`url target href is not allowed: '${rawHref}'. Accepted shapes: ${ALLOWED_HREF_SHAPES_DESCRIPTION}.`);
249
+ if (!isAllowedHref({ rawHref: rawHref })) {
250
+ throw new MenuValidationError({ message: `url target href is not allowed: '${rawHref}'. Accepted shapes: ${ALLOWED_HREF_SHAPES_DESCRIPTION}.` });
240
251
  }
241
252
  }
242
253
  }
243
254
  function assertValidTitleAndSlug(title, slug) {
244
255
  if (!title)
245
- throw new MenuValidationError("title is required");
256
+ throw new MenuValidationError({ message: "title is required" });
246
257
  if (!slug.match(/^[a-z0-9-]+$/)) {
247
- throw new MenuValidationError("slug must use lowercase letters, numbers, and dashes");
258
+ throw new MenuValidationError({ message: "slug must use lowercase letters, numbers, and dashes" });
248
259
  }
249
260
  }
250
261
  /**
@@ -269,8 +280,8 @@ export async function createMenu(required, optional = {}) {
269
280
  assertValidTitleAndSlug(title, slug);
270
281
  const duplicate = await deps.repo.findBySlug({ workspaceId: input.workspaceId, slug });
271
282
  if (duplicate)
272
- throw new MenuConflictError(`slug '${slug}' already exists`);
273
- const items = validateAndCloneTree(input.items ?? [], optional.limits);
283
+ throw new MenuConflictError({ message: `slug '${slug}' already exists` });
284
+ const items = validateAndCloneTree({ items: input.items ?? [] }, optional.limits);
274
285
  const menu = {
275
286
  id: deps.idGen.newId(),
276
287
  workspaceId: input.workspaceId,
@@ -279,7 +290,7 @@ export async function createMenu(required, optional = {}) {
279
290
  status: "published",
280
291
  doc: { type: NAV_DOC_TYPE, version: 1, items },
281
292
  locations: [],
282
- updatedAt: deps.clock.nowIso(),
293
+ updatedAt: kernelNowIso({ clock: deps.clock }),
283
294
  version: 1,
284
295
  };
285
296
  await deps.repo.save(menu);
@@ -304,7 +315,7 @@ export async function createMenu(required, optional = {}) {
304
315
  * previous tree and is deferred — a caller-discipline requirement for now,
305
316
  * not a runtime-enforced invariant.
306
317
  *
307
- * @complexity O(n) in the new tree size for validation; O(1) additional repo
318
+ * @complexity O(n) in the new tree size for validation; O additional repo
308
319
  * calls (one read, at most one slug-uniqueness read, one write).
309
320
  * @overallScore 90
310
321
  * @findings Medium: id-stability across edits (ids not renumbered on survive)
@@ -317,10 +328,10 @@ export async function updateMenuTree(required, optional = {}) {
317
328
  const { deps, input } = required;
318
329
  const existing = await deps.repo.findById({ workspaceId: input.workspaceId, id: input.id });
319
330
  if (!existing)
320
- throw new MenuNotFoundError(`menu '${input.id}' was not found`);
331
+ throw new MenuNotFoundError({ message: `menu '${input.id}' was not found` });
321
332
  assertEntityLive({ entityType: "menu", entityId: input.id, state: existing.status === "trash" ? "trashed" : "live" });
322
333
  if (existing.version !== input.expectedVersion) {
323
- throw new MenuConflictError(`menu '${input.id}' was modified concurrently (expected version ${input.expectedVersion}, found ${existing.version})`);
334
+ throw new MenuConflictError({ message: `menu '${input.id}' was modified concurrently (expected version ${input.expectedVersion}, found ${existing.version})` });
324
335
  }
325
336
  const title = (input.title ?? existing.title).trim();
326
337
  const slug = (input.slug ?? existing.slug).trim().toLowerCase();
@@ -328,16 +339,16 @@ export async function updateMenuTree(required, optional = {}) {
328
339
  if (slug !== existing.slug) {
329
340
  const duplicate = await deps.repo.findBySlug({ workspaceId: input.workspaceId, slug });
330
341
  if (duplicate && duplicate.id !== existing.id) {
331
- throw new MenuConflictError(`slug '${slug}' already exists`);
342
+ throw new MenuConflictError({ message: `slug '${slug}' already exists` });
332
343
  }
333
344
  }
334
- const items = validateAndCloneTree(input.items, optional.limits);
345
+ const items = validateAndCloneTree({ items: input.items }, optional.limits);
335
346
  const menu = {
336
347
  ...existing,
337
348
  title,
338
349
  slug,
339
350
  doc: { type: NAV_DOC_TYPE, version: existing.doc.version, items },
340
- updatedAt: deps.clock.nowIso(),
351
+ updatedAt: kernelNowIso({ clock: deps.clock }),
341
352
  version: existing.version + 1,
342
353
  };
343
354
  await deps.repo.save(menu);
@@ -372,7 +383,7 @@ export async function updateMenuTree(required, optional = {}) {
372
383
  * transaction machinery — do not copy this sequencing pattern into code that
373
384
  * has real transactions available.
374
385
  *
375
- * @complexity O(1) repo calls (bounded: at most two menu reads/writes plus one
386
+ * @complexity O repo calls (bounded: at most two menu reads/writes plus one
376
387
  * binding upsert), independent of workspace size.
377
388
  * @overallScore 90
378
389
  * @findings Medium: no rollback across the two writes (menu save, then
@@ -385,9 +396,9 @@ export async function assignLocation(required, _optional = {}) {
385
396
  const { deps, input } = required;
386
397
  const menu = await deps.repo.findById({ workspaceId: input.workspaceId, id: input.menuId });
387
398
  if (!menu)
388
- throw new MenuNotFoundError(`menu '${input.menuId}' was not found`);
399
+ throw new MenuNotFoundError({ message: `menu '${input.menuId}' was not found` });
389
400
  assertEntityLive({ entityType: "menu", entityId: input.menuId, state: menu.status === "trash" ? "trashed" : "live" });
390
- const now = deps.clock.nowIso();
401
+ const now = kernelNowIso({ clock: deps.clock });
391
402
  const existingBinding = await deps.bindingRepo.findByLocation({
392
403
  workspaceId: input.workspaceId,
393
404
  locationKey: input.locationKey,
@@ -447,17 +458,17 @@ export async function assignLocation(required, _optional = {}) {
447
458
  /**
448
459
  * Deletion ladder (mirrors the media library's):
449
460
  * 1. First call on a non-trashed menu **soft-deletes** it (`status: 'trash'`,
450
- * revisioned) and returns — no purge yet.
451
- * 2. A second call on an already-trashed menu attempts the **hard purge**. If
452
- * the menu is still bound to any location, purge is rejected with
453
- * `MenuLocationBoundError` (409-style) listing the bound keys, unless
454
- * `force` is set — permission-gating that error behind
455
- * `navigation.delete.force` is the caller's (gateway's) job, not this
456
- * function's; this function only enforces the *shape* of the guard.
461
+ * revisioned) and returns — no purge yet.
462
+ * 2. A second call on an already-trashed menu attempts the **hard purge** If
463
+ * the menu is still bound to any location, purge is rejected with
464
+ * `MenuLocationBoundError` (409-style) listing the bound keys, unless
465
+ * `force` is set — permission-gating that error behind
466
+ * `navigation.delete.force` is the caller's (gateway's) job, not this
467
+ * function's; this function only enforces the *shape* of the guard.
457
468
  * 3. On a successful purge, the menu row and all of its location bindings are
458
- * removed.
469
+ * removed.
459
470
  *
460
- * @complexity O(1) repo calls; O(k) bindings scanned where k = locations this
471
+ * @complexity O repo calls; O(k) bindings scanned where k = locations this
461
472
  * menu holds (small, bounded by registered locations).
462
473
  * @overallScore 100
463
474
  */
@@ -465,12 +476,12 @@ export async function deleteMenu(required, _optional = {}) {
465
476
  const { deps, input } = required;
466
477
  const existing = await deps.repo.findById({ workspaceId: input.workspaceId, id: input.id });
467
478
  if (!existing)
468
- throw new MenuNotFoundError(`menu '${input.id}' was not found`);
479
+ throw new MenuNotFoundError({ message: `menu '${input.id}' was not found` });
469
480
  if (existing.status !== "trash") {
470
481
  const trashed = {
471
482
  ...existing,
472
483
  status: "trash",
473
- updatedAt: deps.clock.nowIso(),
484
+ updatedAt: kernelNowIso({ clock: deps.clock }),
474
485
  version: existing.version + 1,
475
486
  };
476
487
  await deps.repo.save(trashed);
@@ -490,7 +501,7 @@ export async function deleteMenu(required, _optional = {}) {
490
501
  });
491
502
  if (bindings.length > 0 && !input.force) {
492
503
  const boundLocations = bindings.map((row) => row.locationKey);
493
- throw new MenuLocationBoundError(`menu '${input.id}' is still bound to location(s): ${boundLocations.join(", ")} — unassign before purging, or use force`, boundLocations);
504
+ throw new MenuLocationBoundError({ message: `menu '${input.id}' is still bound to location(s): ${boundLocations.join(", ")} — unassign before purging, or use force`, boundLocations: boundLocations });
494
505
  }
495
506
  await deps.repo.remove({ workspaceId: input.workspaceId, id: input.id });
496
507
  await deps.bindingRepo.removeByMenu({ workspaceId: input.workspaceId, menuId: input.id });
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * INTERFACES ONLY. No feature logic lives here.
24
24
  */
25
- import type { ISODateTime, UUID } from "../core/ports.js";
25
+ import type { ISODateTime, UUID } from "@jini-ai/core/primitives";
26
26
  import type { NavLocationBindingRow, NavLocationDescriptor, NavLocationKey, NavMenuDoc, NavMenuEntry, ResolvedNav } from "./types.js";
27
27
  /**
28
28
  * Persistence for the derived `nav_location_bindings` index. Writes happen only inside the same transaction as the menu-entry
@@ -4,8 +4,10 @@ import { type ResolveTargetHrefFn } from "./resolver.js";
4
4
  export interface NavMenuReadModelDeps {
5
5
  menuRepo: MenuRepoPort;
6
6
  bindingRepo: NavLocationBindingRepoPort;
7
+ }
8
+ export interface NavMenuReadModelOptions {
7
9
  /** Overridable for tests / once a host's routing library lands; defaults to the honest placeholder documented above. */
8
10
  resolveTargetHref?: ResolveTargetHrefFn | undefined;
9
11
  }
10
- export declare function createNavMenuReadModel(deps: NavMenuReadModelDeps): NavMenuReadModel;
12
+ export declare function createNavMenuReadModel(deps: NavMenuReadModelDeps, optional?: NavMenuReadModelOptions): NavMenuReadModel;
11
13
  //# sourceMappingURL=read-model.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { resolveForLocation } from "./resolver.js";
2
2
  const DEFAULT_RESOLVE_TARGET_HREF = async () => null;
3
- export function createNavMenuReadModel(deps) {
4
- const resolveTargetHref = deps.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
3
+ export function createNavMenuReadModel(deps, optional = {}) {
4
+ const resolveTargetHref = optional.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
5
5
  return {
6
6
  async getMenu(required) {
7
7
  return deps.menuRepo.findById({ workspaceId: required.workspaceId, id: required.menuId });
@@ -1,4 +1,4 @@
1
- import type { ClockPort, UUID } from "../core/ports.js";
1
+ import type { Clock, UUID } from "@jini-ai/core/primitives";
2
2
  import type { MenuRepoPort } from "./repo.memory.js";
3
3
  import type { NavLocationBindingRepoPort } from "./ports.js";
4
4
  /**
@@ -32,7 +32,7 @@ import type { NavLocationBindingRepoPort } from "./ports.js";
32
32
  export interface RebuildNavLocationBindingsDeps {
33
33
  menuRepo: MenuRepoPort;
34
34
  bindingRepo: NavLocationBindingRepoPort;
35
- clock: ClockPort;
35
+ clock: Clock;
36
36
  workspaceId: UUID;
37
37
  }
38
38
  export interface RebuildNavLocationBindingsResult {
@@ -1,3 +1,4 @@
1
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
1
2
  /**
2
3
  * Recompute the workspace's `nav_location_bindings` index from scratch from
3
4
  * every menu's `.locations` field, and replace the stored index with it.
@@ -14,7 +15,7 @@
14
15
  */
15
16
  export async function rebuildNavLocationBindings(deps) {
16
17
  const menus = await deps.menuRepo.list({ workspaceId: deps.workspaceId });
17
- const boundAt = deps.clock.nowIso();
18
+ const boundAt = kernelNowIso({ clock: deps.clock });
18
19
  const byLocation = new Map();
19
20
  for (const menu of menus) {
20
21
  for (const locationKey of menu.locations) {
@@ -31,7 +31,7 @@
31
31
  * `menu-service.ts`'s job) — these classes are dumb, uniqueness-enforcing
32
32
  * collections.
33
33
  */
34
- import type { UUID } from "../core/ports.js";
34
+ import type { UUID } from "@jini-ai/core/primitives";
35
35
  import type { NavLocationBindingRepoPort } from "./ports.js";
36
36
  import type { NavLocationBindingRow, NavLocationKey, NavMenuEntry } from "./types.js";
37
37
  /**
@@ -72,7 +72,9 @@ export interface MenuRepoPort {
72
72
  */
73
73
  export declare class InMemoryMenuRepo implements MenuRepoPort {
74
74
  private rows;
75
- constructor(initialRows?: NavMenuEntry[]);
75
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
76
+ initialRows?: NavMenuEntry[];
77
+ });
76
78
  findById(required: {
77
79
  workspaceId: UUID;
78
80
  id: UUID;
@@ -103,7 +105,9 @@ export declare class InMemoryMenuRepo implements MenuRepoPort {
103
105
  */
104
106
  export declare class InMemoryNavLocationBindingRepo implements NavLocationBindingRepoPort {
105
107
  private rows;
106
- constructor(initialRows?: NavLocationBindingRow[]);
108
+ constructor(requiredArgs: Record<string, never>, optionalArgs?: {
109
+ initialRows?: NavLocationBindingRow[];
110
+ });
107
111
  findByLocation(required: {
108
112
  workspaceId: UUID;
109
113
  locationKey: NavLocationKey;
@@ -10,7 +10,8 @@
10
10
  */
11
11
  export class InMemoryMenuRepo {
12
12
  rows;
13
- constructor(initialRows = []) {
13
+ constructor(requiredArgs, optionalArgs = {}) {
14
+ const { initialRows = [] } = optionalArgs;
14
15
  this.rows = [...initialRows];
15
16
  }
16
17
  async findById(required) {
@@ -50,7 +51,8 @@ export class InMemoryMenuRepo {
50
51
  */
51
52
  export class InMemoryNavLocationBindingRepo {
52
53
  rows;
53
- constructor(initialRows = []) {
54
+ constructor(requiredArgs, optionalArgs = {}) {
55
+ const { initialRows = [] } = optionalArgs;
54
56
  this.rows = [...initialRows];
55
57
  }
56
58
  async findByLocation(required) {
@@ -54,7 +54,10 @@ export interface ResolvedTargetHref {
54
54
  * `null` if it cannot be resolved at all (deleted target, or — for `termRef`
55
55
  * today — no schema yet to resolve against).
56
56
  */
57
- export type ResolveTargetHrefFn = (target: NavTarget, context: NavResolveContext) => Promise<ResolvedTargetHref | null>;
57
+ export type ResolveTargetHrefFn = (required: {
58
+ target: NavTarget;
59
+ context: NavResolveContext;
60
+ }, optional?: Record<string, never>) => Promise<ResolvedTargetHref | null>;
58
61
  export interface ResolveForLocationDeps {
59
62
  menuRepo: MenuRepoPort;
60
63
  bindingRepo: NavLocationBindingRepoPort;
@@ -105,7 +105,7 @@ async function resolveItem(node, context, resolveTargetHref) {
105
105
  // code. termRef in particular has no compatible entry_refs schema today
106
106
  // — a resolver with no term schema to
107
107
  // consult returns null here, same as any other "cannot resolve" outcome.
108
- const result = await resolveTargetHref(node.target, context);
108
+ const result = await resolveTargetHref({ target: node.target, context });
109
109
  if (result === null) {
110
110
  href = null;
111
111
  available = false;
@@ -1,23 +1,21 @@
1
+ import type { IdGenerator } from "@jini-ai/core/primitives";
2
+ import type { Clock } from "@jini-ai/core/primitives";
1
3
  import type { AuthorizeFn } from "../core/commands/command.js";
2
4
  import type { OutboxPort } from "../core/ports.js";
3
- import { type DerivedRiskByToolId, type ToolRegistration } from "../core/tools/registration-kit.js";
5
+ import { type DerivedRiskByToolId, type ToolRegistration } from "@jini-ai/core";
4
6
  import type { NavLocationBindingRepoPort } from "./ports.js";
5
7
  import type { MenuRepoPort } from "./repo.memory.js";
6
8
  /**
7
9
  * The exact slice of the route-deps bag Menus' tool handlers read. Declared structurally (rather
8
10
  * than importing a host's own `RouteDeps`) so this module carries no back-edge into any host's
9
11
  * composition root. A host's `server/routes/*` satisfies this structurally by passing its existing
10
- * route-deps object; nothing there needs to change shape.
12
+ * route-deps object after binding the kernel clock/ID contracts.
11
13
  */
12
14
  export interface MenusToolDeps {
13
15
  authorize: AuthorizeFn;
14
16
  workspaceId: string;
15
- clock: {
16
- nowIso(): string;
17
- };
18
- idGen: {
19
- newId(): string;
20
- };
17
+ clock: Clock;
18
+ idGen: IdGenerator;
21
19
  outbox: OutboxPort;
22
20
  menuRepo: MenuRepoPort;
23
21
  navLocationBindingRepo: NavLocationBindingRepoPort;