@jini-ai/cms 0.5.4 → 0.5.6

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 (244) hide show
  1. package/dist/consent/express/index.d.ts +53 -0
  2. package/dist/consent/express/index.js +159 -0
  3. package/dist/consent/express/response.d.ts +21 -0
  4. package/dist/consent/express/response.js +89 -0
  5. package/dist/consent/html/client.d.ts +6 -0
  6. package/dist/consent/html/client.js +135 -0
  7. package/dist/consent/html/gate.d.ts +29 -0
  8. package/dist/consent/html/gate.js +104 -0
  9. package/dist/consent/html/index.d.ts +4 -0
  10. package/dist/consent/html/index.js +3 -0
  11. package/dist/consent/html/render.d.ts +27 -0
  12. package/dist/consent/html/render.js +62 -0
  13. package/dist/consent/html/static.d.ts +7 -0
  14. package/dist/consent/html/static.js +25 -0
  15. package/dist/consent/html/styles.d.ts +3 -0
  16. package/dist/consent/html/styles.js +22 -0
  17. package/dist/consent/index.d.ts +3 -0
  18. package/dist/consent/index.js +3 -0
  19. package/dist/consent/node/index.d.ts +47 -0
  20. package/dist/consent/node/index.js +169 -0
  21. package/dist/consent/policy.d.ts +47 -0
  22. package/dist/consent/policy.js +62 -0
  23. package/dist/consent/types.d.ts +40 -0
  24. package/dist/consent/types.js +5 -0
  25. package/dist/content-types/agent-tools.js +1 -1
  26. package/dist/content-types/tool-registrations.d.ts +2 -0
  27. package/dist/content-types/tool-registrations.js +1 -0
  28. package/dist/content-types/write-service.d.ts +2 -0
  29. package/dist/content-types/write-service.js +3 -0
  30. package/dist/forms/descriptor-rules.d.ts +9 -0
  31. package/dist/forms/descriptor-rules.js +112 -0
  32. package/dist/forms/email-format.d.ts +7 -0
  33. package/dist/forms/email-format.js +11 -0
  34. package/dist/forms/express/handler.js +4 -8
  35. package/dist/forms/field-registry.d.ts +193 -0
  36. package/dist/forms/field-registry.js +231 -0
  37. package/dist/forms/forms.d.ts +8 -5
  38. package/dist/forms/forms.js +29 -46
  39. package/dist/forms/html/html-authoring.js +63 -13
  40. package/dist/forms/html/html-submission-adapter.d.ts +5 -5
  41. package/dist/forms/html/html-submission-adapter.js +6 -13
  42. package/dist/forms/html-render.d.ts +10 -3
  43. package/dist/forms/html-render.js +116 -14
  44. package/dist/forms/index.d.ts +1 -0
  45. package/dist/forms/index.js +1 -0
  46. package/dist/forms/repo.memory.js +4 -4
  47. package/dist/forms/submission-attempts.d.ts +1 -1
  48. package/dist/forms/submission-attempts.js +13 -2
  49. package/dist/forms/submission-email-validation.js +2 -4
  50. package/dist/forms/submit-service.d.ts +3 -0
  51. package/dist/forms/submit-service.js +38 -5
  52. package/dist/forms/types.d.ts +50 -2
  53. package/dist/listing/batch.d.ts +53 -0
  54. package/dist/listing/batch.js +57 -0
  55. package/dist/listing/html.d.ts +72 -0
  56. package/dist/listing/html.js +109 -0
  57. package/dist/listing/index.d.ts +5 -0
  58. package/dist/listing/index.js +5 -0
  59. package/dist/listing/paths.d.ts +32 -0
  60. package/dist/listing/paths.js +37 -0
  61. package/dist/listing/see-more-script.d.ts +16 -0
  62. package/dist/listing/see-more-script.js +62 -0
  63. package/dist/media/content-type-sniffer.d.ts +1 -1
  64. package/dist/media/content-type-sniffer.js +15 -0
  65. package/dist/media/image-transformer.sharp.js +4 -1
  66. package/dist/media/media-service.d.ts +4 -0
  67. package/dist/media/media-service.js +8 -0
  68. package/dist/media/node.d.ts +1 -1
  69. package/dist/media/node.js +1 -1
  70. package/dist/media/transform-registry.js +1 -1
  71. package/dist/media/transform-types.d.ts +2 -0
  72. package/dist/media/transform-types.js +3 -0
  73. package/dist/media/video-frames.ffmpeg.d.ts +15 -2
  74. package/dist/media/video-frames.ffmpeg.js +28 -13
  75. package/dist/navigation/agent-tools.js +20 -3
  76. package/dist/navigation/dropdown.d.ts +41 -0
  77. package/dist/navigation/dropdown.js +85 -0
  78. package/dist/navigation/index.d.ts +5 -1
  79. package/dist/navigation/index.js +4 -0
  80. package/dist/navigation/menu-html.d.ts +31 -0
  81. package/dist/navigation/menu-html.js +53 -0
  82. package/dist/navigation/menu-service.d.ts +9 -3
  83. package/dist/navigation/menu-service.js +15 -3
  84. package/dist/navigation/tool-registrations.d.ts +5 -0
  85. package/dist/navigation/tool-registrations.js +32 -2
  86. package/dist/navigation/types.d.ts +6 -0
  87. package/dist/redirects/capture.d.ts +2 -0
  88. package/dist/redirects/capture.js +4 -0
  89. package/dist/redirects/redirects.d.ts +8 -0
  90. package/dist/redirects/redirects.js +14 -0
  91. package/dist/routing/index.d.ts +5 -0
  92. package/dist/routing/index.js +2 -0
  93. package/dist/routing/ports.d.ts +51 -0
  94. package/dist/routing/routing.d.ts +138 -0
  95. package/dist/routing/routing.js +443 -0
  96. package/dist/routing/types.d.ts +164 -0
  97. package/dist/routing/types.js +2 -0
  98. package/dist/seo/media.d.ts +23 -0
  99. package/dist/seo/media.js +30 -5
  100. package/dist/seo/page-head.js +8 -1
  101. package/dist/seo/seo.d.ts +2 -0
  102. package/dist/seo/seo.js +11 -6
  103. package/dist/seo/types.d.ts +8 -0
  104. package/dist/seo/xml-escape.d.ts +1 -3
  105. package/dist/seo/xml-escape.js +1 -6
  106. package/dist/widgets/concurrency.d.ts +27 -1
  107. package/dist/widgets/concurrency.js +12 -23
  108. package/dist/widgets/config-validation.js +6 -0
  109. package/dist/widgets/gallery-resolver.d.ts +23 -0
  110. package/dist/widgets/gallery-resolver.js +80 -0
  111. package/dist/widgets/html/gallery-render.d.ts +24 -0
  112. package/dist/widgets/html/gallery-render.js +65 -0
  113. package/dist/widgets/html/html-embeds.d.ts +4 -0
  114. package/dist/widgets/html/html-embeds.js +7 -1
  115. package/dist/widgets/html/index.d.ts +3 -0
  116. package/dist/widgets/html/index.js +3 -0
  117. package/dist/widgets/html/server-fragment.d.ts +9 -0
  118. package/dist/widgets/html/server-fragment.js +38 -0
  119. package/dist/widgets/markers/gallery.d.ts +50 -0
  120. package/dist/widgets/markers/gallery.js +164 -0
  121. package/dist/widgets/markers/html-attributes.d.ts +6 -1
  122. package/dist/widgets/markers/html-attributes.js +24 -9
  123. package/dist/widgets/markers/index.d.ts +2 -0
  124. package/dist/widgets/markers/index.js +2 -0
  125. package/dist/widgets/markers/marker.d.ts +4 -0
  126. package/dist/widgets/markers/marker.js +6 -0
  127. package/dist/widgets/markers/media-reference.d.ts +11 -0
  128. package/dist/widgets/markers/media-reference.js +11 -0
  129. package/dist/widgets/page-embed-types.d.ts +1 -1
  130. package/dist/widgets/page-embed-types.js +1 -1
  131. package/dist/widgets/registry.d.ts +144 -10
  132. package/dist/widgets/registry.js +40 -0
  133. package/dist/widgets/resolver-service.d.ts +10 -0
  134. package/dist/widgets/resolver-service.js +73 -29
  135. package/dist/widgets/resolvers/create-core-resolvers.d.ts +3 -0
  136. package/dist/widgets/resolvers/create-core-resolvers.js +2 -0
  137. package/dist/widgets/resolvers/gallery.d.ts +12 -0
  138. package/dist/widgets/resolvers/gallery.js +33 -0
  139. package/dist/widgets/resolvers/index.d.ts +1 -0
  140. package/dist/widgets/resolvers/index.js +1 -0
  141. package/dist/widgets/resolvers/menu.d.ts +8 -0
  142. package/dist/widgets/resolvers/menu.js +21 -6
  143. package/dist/widgets/types.d.ts +3 -1
  144. package/package.json +51 -6
  145. package/dist/http/settings/cms-adapter.d.ts +0 -20
  146. package/dist/http/settings/cms-adapter.js +0 -124
  147. package/dist/http/settings/contracts.d.ts +0 -135
  148. package/dist/http/settings/contracts.js +0 -2
  149. package/dist/http/settings/http.d.ts +0 -10
  150. package/dist/http/settings/http.js +0 -211
  151. package/dist/http/settings/index.d.ts +0 -5
  152. package/dist/http/settings/index.js +0 -4
  153. package/dist/http/settings/shared.d.ts +0 -68
  154. package/dist/http/settings/shared.js +0 -54
  155. package/dist/http/settings/stream.d.ts +0 -24
  156. package/dist/http/settings/stream.js +0 -147
  157. package/dist/settings/agent-tools.d.ts +0 -10
  158. package/dist/settings/agent-tools.js +0 -204
  159. package/dist/settings/agent-value-write-tools.d.ts +0 -22
  160. package/dist/settings/agent-value-write-tools.js +0 -154
  161. package/dist/settings/agent-writable-preferences.d.ts +0 -67
  162. package/dist/settings/agent-writable-preferences.js +0 -132
  163. package/dist/settings/agent-write-denylist.d.ts +0 -19
  164. package/dist/settings/agent-write-denylist.js +0 -15
  165. package/dist/settings/change-feed.d.ts +0 -83
  166. package/dist/settings/change-feed.js +0 -61
  167. package/dist/settings/definitions-dispatch.d.ts +0 -56
  168. package/dist/settings/definitions-dispatch.js +0 -86
  169. package/dist/settings/dictionaries/index.d.ts +0 -48
  170. package/dist/settings/dictionaries/index.js +0 -69
  171. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +0 -3
  172. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -27
  173. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +0 -3
  174. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -27
  175. package/dist/settings/dictionaries/settings-dialog.de.d.ts +0 -3
  176. package/dist/settings/dictionaries/settings-dialog.de.js +0 -27
  177. package/dist/settings/dictionaries/settings-dialog.es.d.ts +0 -3
  178. package/dist/settings/dictionaries/settings-dialog.es.js +0 -27
  179. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +0 -3
  180. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -27
  181. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +0 -3
  182. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -27
  183. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +0 -3
  184. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -27
  185. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +0 -3
  186. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -27
  187. package/dist/settings/dictionaries/settings-dialog.id.d.ts +0 -3
  188. package/dist/settings/dictionaries/settings-dialog.id.js +0 -27
  189. package/dist/settings/dictionaries/settings-dialog.it.d.ts +0 -3
  190. package/dist/settings/dictionaries/settings-dialog.it.js +0 -27
  191. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +0 -3
  192. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -27
  193. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +0 -3
  194. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -27
  195. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +0 -3
  196. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -27
  197. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +0 -3
  198. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -27
  199. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +0 -3
  200. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -27
  201. package/dist/settings/dictionaries/settings-dialog.th.d.ts +0 -3
  202. package/dist/settings/dictionaries/settings-dialog.th.js +0 -27
  203. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +0 -3
  204. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -27
  205. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +0 -3
  206. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -27
  207. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +0 -3
  208. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -27
  209. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +0 -3
  210. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -27
  211. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +0 -3
  212. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -27
  213. package/dist/settings/ensure-definitions.d.ts +0 -88
  214. package/dist/settings/ensure-definitions.js +0 -91
  215. package/dist/settings/errors.d.ts +0 -42
  216. package/dist/settings/errors.js +0 -46
  217. package/dist/settings/express/cms-adapter.d.ts +0 -20
  218. package/dist/settings/express/cms-adapter.js +0 -124
  219. package/dist/settings/express/contracts.d.ts +0 -135
  220. package/dist/settings/express/contracts.js +0 -2
  221. package/dist/settings/express/http.d.ts +0 -10
  222. package/dist/settings/express/http.js +0 -211
  223. package/dist/settings/express/shared.d.ts +0 -68
  224. package/dist/settings/express/shared.js +0 -54
  225. package/dist/settings/express/stream.d.ts +0 -24
  226. package/dist/settings/express/stream.js +0 -147
  227. package/dist/settings/ports.d.ts +0 -118
  228. package/dist/settings/principal-lookup.d.ts +0 -9
  229. package/dist/settings/principal-lookup.js +0 -2
  230. package/dist/settings/purge-service.d.ts +0 -54
  231. package/dist/settings/purge-service.js +0 -106
  232. package/dist/settings/repo.memory.d.ts +0 -119
  233. package/dist/settings/repo.memory.js +0 -199
  234. package/dist/settings/settings.d.ts +0 -139
  235. package/dist/settings/settings.js +0 -259
  236. package/dist/settings/tool-registrations.d.ts +0 -51
  237. package/dist/settings/tool-registrations.js +0 -250
  238. package/dist/settings/types.d.ts +0 -125
  239. package/dist/settings/types.js +0 -3
  240. package/dist/settings/ui-tab-definitions.d.ts +0 -41
  241. package/dist/settings/ui-tab-definitions.js +0 -148
  242. package/dist/settings/write-service.d.ts +0 -320
  243. package/dist/settings/write-service.js +0 -683
  244. /package/dist/{settings → routing}/ports.js +0 -0
@@ -19,6 +19,11 @@ export interface MenusToolDeps {
19
19
  outbox: OutboxPort;
20
20
  menuRepo: MenuRepoPort;
21
21
  navLocationBindingRepo: NavLocationBindingRepoPort;
22
+ /**
23
+ * The host's raw-HTML permission (Tovu: `pages.edit_html`, the one HTML-mode forms use). A write
24
+ * carrying menu HTML is checked against it on top of the menu permission; unset, HTML menus are refused.
25
+ */
26
+ rawHtmlPermission?: string | undefined;
22
27
  }
23
28
  /**
24
29
  * This wiring layer's OWN risk classification, authored from what each handler below actually
@@ -15,6 +15,7 @@ import { buildDomainRegistrations, indexCatalogById, requireInputRecord, require
15
15
  import { requireToolPermission } from "../core/tools/index.js";
16
16
  import { menusAgentToolCatalog } from "./agent-tools.js";
17
17
  import { assignLocation, createMenu, MenuNotFoundError, MenuValidationError, updateMenuTree } from "./menu-service.js";
18
+ import { isMenuHtmlAuthoring } from "./menu-html.js";
18
19
  const CATALOG_BY_ID = indexCatalogById({ catalog: menusAgentToolCatalog });
19
20
  /**
20
21
  * This wiring layer's OWN risk classification, authored from what each handler below actually
@@ -54,10 +55,30 @@ function toMenuToolView(menu) {
54
55
  title: menu.title,
55
56
  status: menu.status,
56
57
  items: JSON.parse(JSON.stringify(menu.doc.items)),
58
+ ...(menu.doc.mode !== undefined ? { mode: menu.doc.mode } : {}),
59
+ ...(menu.doc.html !== undefined ? { html: menu.doc.html } : {}),
57
60
  locations: [...menu.locations],
58
61
  version: menu.version,
59
62
  };
60
63
  }
64
+ /** A write's HTML-mode fields as sent; `menu-service.ts` validates their values. */
65
+ function htmlAuthoring(input) {
66
+ return {
67
+ ...(input.mode !== undefined ? { mode: input.mode } : {}),
68
+ ...(input.html !== undefined ? { html: input.html } : {}),
69
+ };
70
+ }
71
+ /**
72
+ * Raw HTML is the host's trust boundary (HTML Pages, HTML-mode forms), so a write that authors menu
73
+ * HTML also needs that permission. Checked before the write, like forms' `prepareFormAuthoring`.
74
+ */
75
+ async function requireHtmlAuthoringPermission({ routeDeps, principalId, authoring, menuId }) {
76
+ if (!isMenuHtmlAuthoring(authoring))
77
+ return;
78
+ if (!routeDeps.rawHtmlPermission)
79
+ throw new ToolInputError({ message: "HTML menus are not enabled on this site" });
80
+ await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId, permission: routeDeps.rawHtmlPermission }, { entityType: "menu", ...(menuId ? { entityId: menuId } : {}) });
81
+ }
61
82
  function menusDeps(routeDeps) {
62
83
  return { repo: routeDeps.menuRepo, clock: routeDeps.clock, idGen: routeDeps.idGen, outbox: routeDeps.outbox };
63
84
  }
@@ -82,6 +103,8 @@ export function buildMenusRegistrations(routeDeps) {
82
103
  if (input.items !== undefined && !Array.isArray(input.items)) {
83
104
  throw new ToolInputError({ message: "'items' must be an array of nav items" });
84
105
  }
106
+ const authoring = htmlAuthoring(input);
107
+ await requireHtmlAuthoringPermission({ routeDeps, principalId: ctx.principal.id, authoring });
85
108
  return withSchemaOnRejection({ toolId: "menus_create_menu", catalog: CATALOG_BY_ID, isShapeRejection: ({ error }) => isMenusShapeRejection(error), fn: async () => {
86
109
  const { menu } = await createMenu({
87
110
  deps: menusDeps(routeDeps),
@@ -90,6 +113,7 @@ export function buildMenusRegistrations(routeDeps) {
90
113
  title: requireString({ input: input, key: "title" }),
91
114
  slug: requireString({ input: input, key: "slug" }),
92
115
  items: Array.isArray(input.items) ? input.items : undefined,
116
+ ...authoring,
93
117
  },
94
118
  });
95
119
  return { menu: toMenuToolView(menu) };
@@ -99,8 +123,13 @@ export function buildMenusRegistrations(routeDeps) {
99
123
  const input = requireInputRecord({ input: ctx.input });
100
124
  const menuId = requireString({ input: input, key: "menuId" });
101
125
  await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: ctx.principal.id, permission: "admin.menus.update" }, { entityType: "menu", entityId: menuId });
102
- if (!Array.isArray(input.items))
103
- throw new Error("'items' (array) is required");
126
+ const authoring = htmlAuthoring(input);
127
+ if (input.items === undefined && authoring.mode === undefined && authoring.html === undefined) {
128
+ throw new Error("'items' (array), 'html' or 'mode' is required");
129
+ }
130
+ if (input.items !== undefined && !Array.isArray(input.items))
131
+ throw new Error("'items' must be an array of nav items");
132
+ await requireHtmlAuthoringPermission({ routeDeps, principalId: ctx.principal.id, authoring, menuId });
104
133
  return withSchemaOnRejection({ toolId: "menus_update_menu_tree", catalog: CATALOG_BY_ID, isShapeRejection: ({ error }) => isMenusShapeRejection(error), fn: async () => {
105
134
  const { menu } = await updateMenuTree({
106
135
  deps: menusDeps(routeDeps),
@@ -111,6 +140,7 @@ export function buildMenusRegistrations(routeDeps) {
111
140
  title: typeof input.title === "string" ? input.title : undefined,
112
141
  slug: typeof input.slug === "string" ? input.slug : undefined,
113
142
  items: input.items,
143
+ ...authoring,
114
144
  },
115
145
  });
116
146
  return { menu: toMenuToolView(menu) };
@@ -123,7 +123,13 @@ export interface NavMenuDoc {
123
123
  /** Doc-schema version, so the validator can migrate shapes later. */
124
124
  readonly version: number;
125
125
  readonly items: readonly NavItemNode[];
126
+ /** What renders at the menu's marker; absent means `"items"`. See `menu-html.ts`. */
127
+ readonly mode?: NavMenuMode | undefined;
128
+ /** Author-written HTML for `"html"` mode, kept even while the menu renders its items. */
129
+ readonly html?: string | undefined;
126
130
  }
131
+ /** `"items"` renders the item tree; `"html"` renders the author's own markup (`menu-html.ts`). */
132
+ export type NavMenuMode = "items" | "html";
127
133
  export type MenuStatus = "draft" | "published" | "trash";
128
134
  /**
129
135
  * A typed read model over a `type='menu'` entries row (universal
@@ -55,6 +55,8 @@ export interface RedirectSlugChangeCaptureReadDeps {
55
55
  }, optional?: Record<string, never>): Promise<RedirectRecord | null>;
56
56
  }
57
57
  export interface RedirectSlugChangeCaptureDeps {
58
+ /** Optional for existing hosts; route-owning hosts supply their reserved segment policy. */
59
+ reservedSegments?: ReadonlySet<string>;
58
60
  repo: RedirectSlugChangeCaptureReadDeps;
59
61
  db: RedirectDbHandle;
60
62
  clock: ClockPort;
@@ -1,4 +1,5 @@
1
1
  import { nowIso as clockNowIso } from "@jini-ai/core/primitives";
2
+ import { assertRedirectSourceAllowed } from "./redirects.js";
2
3
  import { insertRedirectAndRevision } from "./ports.internal.js";
3
4
  /** Default priority/statusCode/override for an auto-captured rule (behavior.spec.md §3 analog). */
4
5
  const AUTO_CAPTURE_STATUS_CODE = 301;
@@ -21,6 +22,9 @@ export class RedirectSlugChangeCapture {
21
22
  * @complexity O(1) — one read (idempotency check) + two writes.
22
23
  */
23
24
  async onSlugChange(input, _optional = {}) {
25
+ if (this.deps.reservedSegments) {
26
+ assertRedirectSourceAllowed({ fromPattern: input.oldPath, reservedSegments: this.deps.reservedSegments });
27
+ }
24
28
  const existing = await this.deps.repo.lookupExact({
25
29
  workspaceId: input.workspaceId,
26
30
  path: input.oldPath,
@@ -100,6 +100,14 @@ export interface RedirectsWriteDeps {
100
100
  idGen: IdGeneratorPort;
101
101
  outbox: OutboxPort;
102
102
  }
103
+ /** Refuse source patterns whose decoded first segment belongs to a host route.
104
+ * Uses the same decoding and case folding as the target gate; wildcard tails remain patterns.
105
+ * @throws RedirectValidationError before any write. @complexity O(n) in the pattern length.
106
+ */
107
+ export declare function assertRedirectSourceAllowed({ fromPattern, reservedSegments }: {
108
+ fromPattern: string;
109
+ reservedSegments: ReadonlySet<string>;
110
+ }, _optional?: Record<string, never>): void;
103
111
  /**
104
112
  * The site-relative half of the write gate: routing's structural + reserved-path verdict, then the
105
113
  * redirect oracle's own raw-character rule. The read path hands a relative location to that oracle
@@ -144,6 +144,16 @@ function siteRelativeRefusalReason(check) {
144
144
  return "it contains, or decodes to, a backslash or a control character";
145
145
  }
146
146
  }
147
+ /** Refuse source patterns whose decoded first segment belongs to a host route.
148
+ * Uses the same decoding and case folding as the target gate; wildcard tails remain patterns.
149
+ * @throws RedirectValidationError before any write. @complexity O(n) in the pattern length.
150
+ */
151
+ export function assertRedirectSourceAllowed({ fromPattern, reservedSegments }, _optional = {}) {
152
+ const verdict = checkSiteRelativeTarget({ raw: fromPattern, reservedSegments }, {});
153
+ if (verdict.kind === "reserved") {
154
+ throw new RedirectValidationError(`fromPattern '${fromPattern}' is reserved for the '${verdict.surface}' surface`);
155
+ }
156
+ }
147
157
  /**
148
158
  * The site-relative half of the write gate: routing's structural + reserved-path verdict, then the
149
159
  * redirect oracle's own raw-character rule. The read path hands a relative location to that oracle
@@ -257,6 +267,7 @@ export async function checkRedirectFieldsWouldWrite({ deps, fields }, { excludeI
257
267
  validateToTargetLength(fields.toTarget);
258
268
  validateStatusCode(fields.statusCode);
259
269
  validatePriority(fields.priority);
270
+ assertRedirectSourceAllowed({ fromPattern: fields.fromPattern, reservedSegments: deps.reservedSegments });
260
271
  await assertTargetAllowed(deps.originRegistry, fields.workspaceId, fields.toTarget, deps.reservedSegments);
261
272
  const finalTarget = await resolveCollapsedTarget(deps.repo, fields.workspaceId, fields.fromPattern, fields.toTarget);
262
273
  if (finalTarget !== fields.toTarget) {
@@ -300,6 +311,7 @@ export async function createRedirect(required, _optional = {}) {
300
311
  validateStatusCode(input.statusCode);
301
312
  const priority = input.priority ?? DEFAULT_PRIORITY;
302
313
  validatePriority(priority);
314
+ assertRedirectSourceAllowed({ fromPattern: input.fromPattern, reservedSegments: deps.reservedSegments });
303
315
  await assertTargetAllowed(deps.originRegistry, input.workspaceId, input.toTarget, deps.reservedSegments);
304
316
  const finalTarget = await resolveCollapsedTarget(deps.repo, input.workspaceId, input.fromPattern, input.toTarget);
305
317
  if (finalTarget !== input.toTarget) {
@@ -411,6 +423,7 @@ async function validateUpdateFields(deps, workspaceId, fields, existingId) {
411
423
  validateStatusCode(fields.statusCode);
412
424
  validateStatus(fields.status);
413
425
  validatePriority(fields.priority);
426
+ assertRedirectSourceAllowed({ fromPattern: fields.fromPattern, reservedSegments: deps.reservedSegments });
414
427
  await assertTargetAllowed(deps.originRegistry, workspaceId, fields.toTarget, deps.reservedSegments);
415
428
  const finalTarget = await resolveCollapsedTarget(deps.repo, workspaceId, fields.fromPattern, fields.toTarget);
416
429
  if (finalTarget !== fields.toTarget) {
@@ -442,6 +455,7 @@ export async function updateRedirect(required, _optional = {}) {
442
455
  if (!isRestoreOnlyUpdate(input)) {
443
456
  assertEntityLive({ entityType: "redirect", entityId: input.id, state: "trashed" });
444
457
  }
458
+ assertRedirectSourceAllowed({ fromPattern: existing.fromPattern, reservedSegments: deps.reservedSegments });
445
459
  const outcome = await deps.restore({
446
460
  workspaceId: input.workspaceId,
447
461
  id: input.id,
@@ -0,0 +1,5 @@
1
+ export { createRouting, RouteResolutionError } from './routing.js';
2
+ export type { CreateRoutingRequired, CreateRoutingOptional, EmptyOptions, Routing, IsActiveOptional, IsActiveRequired, RegisterNamedRouteInput, RegisterResolvePhaseOptions, ResolveInput, ResolveOptional, ResolveRequired, UrlForOptional, UrlForRequired } from './routing.js';
3
+ export type { RouteResolverDeps, RoutingContentRecord, RoutingContentReadPort, RoutingPorts } from './ports.js';
4
+ export type { EntryRefTarget, RouteRefTarget, RouteResolveContext, RouteResolvePhaseHandler, RouteResolvePhaseName, RouteResolvePhaseOutcome, RouteResolveResult, RouteTarget, RouteTargetKind, RouteUrl, SlugChangeCapture, SlugChangeCaptureInput, TermRefTarget, UrlTarget } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { createRouting, RouteResolutionError } from './routing.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @file Dependency contracts for the Tovu `routing` Tier-2 library (ADR-039 v0).
3
+ *
4
+ * ADR-006 accounting (rule-of-two):
5
+ * - `routing` introduces NO new persistence port. Resolving an `entryRef`
6
+ * target reuses the existing `PostRepoPort` (`src/features/post/post.ts`) —
7
+ * `post` is the only content type implemented in this repo today. Inventing
8
+ * a routing-owned repo for data that already has one would fail rule-of-two.
9
+ * - The named-route registry and the resolve-phase/`SlugChangeCapture`
10
+ * registration slots are core-owned registration mechanisms (ADR-039 §1/§4),
11
+ * NOT open ADR-009 plugin hooks and NOT ports — they are ordinary in-module
12
+ * state with a typed register/get surface, same treatment ADR-029/ADR-032
13
+ * give their single-evaluator resolvers.
14
+ *
15
+ * Architectural role:
16
+ * INTERFACES ONLY — no feature logic lives here.
17
+ */
18
+ /** Minimal content projection; the host visibility owner evaluates publication, scheduling and Trash. */
19
+ export interface RoutingContentRecord {
20
+ readonly slug: string;
21
+ readonly status: string;
22
+ readonly deletedAt?: string | null | undefined;
23
+ readonly publishAt?: string | null | undefined;
24
+ }
25
+ /** A projection of the existing content reader, not a new store or persistence owner. */
26
+ export interface RoutingContentReadPort {
27
+ findById(required: {
28
+ workspaceId: string;
29
+ id: string;
30
+ }, optional?: Record<string, never>): Promise<RoutingContentRecord | null>;
31
+ }
32
+ /** Host-owned policy and operator channel. No implicit clock or console in generic routing. */
33
+ export interface RoutingPorts {
34
+ isLive(required: {
35
+ post: RoutingContentRecord;
36
+ }, optional?: Record<string, never>): boolean;
37
+ reportPhaseError(required: {
38
+ message: string;
39
+ error: unknown;
40
+ }, optional?: Record<string, never>): void;
41
+ }
42
+ /**
43
+ * Dependencies `urlFor`/`isActive` need to resolve `entryRef` targets. Kept as
44
+ * its own named shape (rather than inlining `PostRepoPort` at each call site)
45
+ * so a future second content type's repo can be added here without changing
46
+ * every call site's required-object shape.
47
+ */
48
+ export interface RouteResolverDeps {
49
+ readonly postRepo: RoutingContentReadPort;
50
+ }
51
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @file Resolution logic for the Tovu `routing` Tier-2 library (ADR-039 v0).
3
+ *
4
+ * Purpose:
5
+ * Implements the inverse resolver (`urlFor`/`isActive`) Menus (ADR-029) and
6
+ * SEO (ADR-032) consume, the forward resolution pipeline registration seam
7
+ * Redirects (ADR-033) will consume, the named-route registry backing `route`
8
+ * targets, and the `SlugChangeCapture` registration slot (ADR-039 §4).
9
+ *
10
+ * How it relates to the project:
11
+ * - `routing/types.ts` supplies the `RouteTarget` vocabulary and result shapes.
12
+ * - `routing/ports.ts` supplies `RouteResolverDeps` (reuses `PostRepoPort`).
13
+ * - Only `post` is a real implemented content type today, so `entryRef`
14
+ * resolution goes through `PostRepoPort` directly; `termRef` is a documented
15
+ * stub (taxonomy doesn't exist yet — ADR-029 Round-3 fold item 1).
16
+ *
17
+ * Architectural role:
18
+ * Core-owned Tier-2 library code. No Drizzle/SQLite adapter and no HTTP
19
+ * wiring live here (that is out of scope for this v0 build) — this file is
20
+ * pure resolution logic over injected ports and in-module registries.
21
+ */
22
+ import type { RouteResolverDeps, RoutingContentRecord, RoutingPorts } from './ports.js';
23
+ import type { RouteResolveContext, RouteResolvePhaseHandler, RouteResolvePhaseName, RouteResolvePhaseOutcome, RouteResolveResult, RouteTarget, RouteUrl, SlugChangeCapture } from './types.js';
24
+ /** Raised when a `RouteTarget` carries a discriminant this library does not recognize. */
25
+ export declare class RouteResolutionError extends Error {
26
+ }
27
+ export interface RegisterNamedRouteInput {
28
+ readonly name: string;
29
+ readonly path: string;
30
+ }
31
+ export interface UrlForRequired {
32
+ deps: RouteResolverDeps;
33
+ target: RouteTarget;
34
+ ctx: RouteResolveContext;
35
+ }
36
+ export interface UrlForOptional {
37
+ }
38
+ export interface IsActiveRequired {
39
+ deps: RouteResolverDeps;
40
+ target: RouteTarget;
41
+ currentPath: string;
42
+ ctx: RouteResolveContext;
43
+ }
44
+ export interface IsActiveOptional {
45
+ }
46
+ export interface RegisterResolvePhaseOptions {
47
+ /**
48
+ * The registrant's identity. Registering under an owner that already holds
49
+ * handlers in this phase REPLACES them rather than appending, so a registrant
50
+ * whose handlers close over a per-boot resource (a site's database handle, a
51
+ * request-scoped client) cannot leave the previous boot's closures reachable.
52
+ *
53
+ * Omit it and the registration is appended and never superseded — the v0
54
+ * behavior this library's own tests still use.
55
+ */
56
+ readonly owner?: unknown;
57
+ /**
58
+ * This registration's fault policy when its handler throws. `"fail"` (the default) rethrows
59
+ * out of the phase, so the caller's own error handling runs (the public site routes answer
60
+ * `500 <h1>Site error</h1>`, on BOTH phases as of 2026-09-16, t91 F4.3-A: `post_content` on
61
+ * `GET /:slug` used to leave a default-policy throw with no response at all — see
62
+ * `routes/site/pages.ts`'s `handlePostNotFoundInSlugCatch`). `"skip"` logs `[routing] a <phase>
63
+ * resolver failed for <path> — skipping it` and continues to the phase's remaining handlers.
64
+ *
65
+ * Choose `"skip"` only for a handler whose failure can only DECLINE an outcome — it can never
66
+ * fabricate one, so a swallowed failure cannot weaken any guard. Redirects opts into it for
67
+ * exactly that reason (INV-03: a lookup failure means "no redirect", never "gone"). A future
68
+ * guard-type handler (a takedown or reserved-path check, which must BLOCK on failure, not wave
69
+ * a request through) fails CLOSED by default unless it deliberately opts into `"skip"` too.
70
+ */
71
+ readonly onError?: "skip" | "fail";
72
+ }
73
+ export interface ResolveInput {
74
+ path: string;
75
+ ctx: RouteResolveContext;
76
+ }
77
+ export interface ResolveRequired {
78
+ input: ResolveInput;
79
+ }
80
+ export interface ResolveOptional {
81
+ }
82
+ export type EmptyOptions = Record<string, never>;
83
+ export interface CreateRoutingRequired {
84
+ readonly ports: RoutingPorts;
85
+ }
86
+ export interface CreateRoutingOptional {
87
+ readonly namedRoutes?: ReadonlyArray<readonly [string, string]>;
88
+ }
89
+ /** Bound routing API. All methods share one instance's registrations and host ports. */
90
+ export interface Routing {
91
+ entryPublicPath(required: {
92
+ post: RoutingContentRecord;
93
+ ctx: RouteResolveContext;
94
+ }, optional?: EmptyOptions): RouteUrl | null;
95
+ postPublicPath(required: {
96
+ slug: string;
97
+ }, optional?: EmptyOptions): string;
98
+ getNamedRoute(required: {
99
+ name: string;
100
+ }, optional?: EmptyOptions): string | null;
101
+ registerNamedRoute(required: RegisterNamedRouteInput, optional?: EmptyOptions): void;
102
+ urlFor(required: UrlForRequired, optional?: UrlForOptional): Promise<RouteUrl | null>;
103
+ isActive(required: IsActiveRequired, optional?: IsActiveOptional): Promise<boolean>;
104
+ resolve(required: ResolveRequired, optional?: ResolveOptional): Promise<RouteResolveResult>;
105
+ registerResolvePhase(required: {
106
+ phase: RouteResolvePhaseName;
107
+ resolver: RouteResolvePhaseHandler;
108
+ }, optional?: RegisterResolvePhaseOptions): () => void;
109
+ unregisterResolvePhaseOwner(required: {
110
+ owner: unknown;
111
+ }, optional?: EmptyOptions): void;
112
+ runPreContentPhase(required: {
113
+ path: string;
114
+ ctx: RouteResolveContext;
115
+ }, optional?: EmptyOptions): Promise<RouteResolvePhaseOutcome | null>;
116
+ runPostContentPhase(required: {
117
+ path: string;
118
+ ctx: RouteResolveContext;
119
+ }, optional?: EmptyOptions): Promise<RouteResolvePhaseOutcome | null>;
120
+ registerSlugChangeCapture(required: {
121
+ impl: SlugChangeCapture;
122
+ }, optional?: EmptyOptions): () => void;
123
+ getSlugChangeCapture(required: EmptyOptions, optional?: EmptyOptions): SlugChangeCapture | undefined;
124
+ /** Test-only reset of this instance; never resets another instance's registrations. */
125
+ resetRoutingRegistrationsForTests(required: EmptyOptions, optional?: EmptyOptions): void;
126
+ }
127
+ /**
128
+ * Create routing over host-owned content visibility and fault reporting.
129
+ * Each instance owns its registrations; a host that historically shared one process registry
130
+ * must bind one instance and reuse it across composition roots. No database or clock is constructed.
131
+ * @param required Host ports; visibility errors propagate unchanged.
132
+ * @param optional Initial named-route configuration (home/search by default).
133
+ * @returns Resolvers and registration lifecycles sharing this instance's state.
134
+ * @complexity O(r) initialization for r configured named routes.
135
+ * @example const routing = createRouting({ ports }, { namedRoutes: [['home', '/']] });
136
+ */
137
+ export declare function createRouting(required: CreateRoutingRequired, optional?: CreateRoutingOptional): Routing;
138
+ //# sourceMappingURL=routing.d.ts.map