@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
@@ -0,0 +1,443 @@
1
+ /** Raised when a `RouteTarget` carries a discriminant this library does not recognize. */
2
+ export class RouteResolutionError extends Error {
3
+ }
4
+ /**
5
+ * Create routing over host-owned content visibility and fault reporting.
6
+ * Each instance owns its registrations; a host that historically shared one process registry
7
+ * must bind one instance and reuse it across composition roots. No database or clock is constructed.
8
+ * @param required Host ports; visibility errors propagate unchanged.
9
+ * @param optional Initial named-route configuration (home/search by default).
10
+ * @returns Resolvers and registration lifecycles sharing this instance's state.
11
+ * @complexity O(r) initialization for r configured named routes.
12
+ * @example const routing = createRouting({ ports }, { namedRoutes: [['home', '/']] });
13
+ */
14
+ export function createRouting(required, optional = {}) {
15
+ // ---------------------------------------------------------------------------
16
+ // Named-route registry (backs `route` targets — ADR-039 §3)
17
+ // ---------------------------------------------------------------------------
18
+ const DEFAULT_NAMED_ROUTES = [
19
+ ["home", "/"],
20
+ ["search", "/search"],
21
+ ];
22
+ const initialNamedRoutes = optional.namedRoutes ?? DEFAULT_NAMED_ROUTES;
23
+ const namedRoutes = new Map(initialNamedRoutes);
24
+ /**
25
+ * Register (or overwrite) a named core route. Core-owned registration, not an
26
+ * open plugin hook — matches ADR-039 §1's treatment of the forward pipeline.
27
+ *
28
+ * @complexity O(1) time/space.
29
+ */
30
+ function registerNamedRoute(input, _optional = {}) {
31
+ namedRoutes.set(input.name, input.path);
32
+ }
33
+ /**
34
+ * Look up a named route's fixed path.
35
+ *
36
+ * @returns the registered path, or `null` if `name` has no registration
37
+ * (mirrors the library-wide "unavailable → null" contract).
38
+ * @complexity O(1) time/space.
39
+ */
40
+ function getNamedRoute({ name }, _optional = {}) {
41
+ return namedRoutes.get(name) ?? null;
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Path normalization + canonical URL composition
45
+ // ---------------------------------------------------------------------------
46
+ const ABSOLUTE_URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
47
+ function isAbsoluteUrl(value) {
48
+ return ABSOLUTE_URL_PATTERN.test(value) || value.startsWith("//");
49
+ }
50
+ /**
51
+ * Normalize a request/target path: strip query/hash, force a leading slash,
52
+ * and drop a trailing slash (except for the root path itself).
53
+ *
54
+ * @complexity O(n) in path length.
55
+ */
56
+ function normalizePath(rawPath) {
57
+ const withoutQueryOrHash = rawPath.split(/[?#]/)[0] ?? "";
58
+ const withLeadingSlash = withoutQueryOrHash.startsWith("/")
59
+ ? withoutQueryOrHash
60
+ : `/${withoutQueryOrHash}`;
61
+ if (withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")) {
62
+ return withLeadingSlash.slice(0, -1);
63
+ }
64
+ return withLeadingSlash;
65
+ }
66
+ /**
67
+ * Builds a post/page's own public path from its `slug` — the one point every call site that turns a
68
+ * `PostRecord.slug` into a route string goes through, so the content-owned-homepage `"/"` root-slug
69
+ * exception (`post.ts`'s `ROOT_SLUG`) has exactly one implementation instead of N independently
70
+ * re-deriving it. `/${slug}` is correct for every ordinary slug; the one deliberate exception is the
71
+ * literal `"/"` slug itself, which a naive `/${slug}` template would double up into `"//"` — a Page
72
+ * that has claimed the site's own homepage (`post.ts`'s `ROOT_SLUG` doc has the write-time gate that
73
+ * makes `"/"` the only slug this can ever matter for).
74
+ *
75
+ * @complexity O(1).
76
+ */
77
+ function postPublicPath({ slug }, _optional = {}) {
78
+ return slug === "/" ? "/" : `/${slug}`;
79
+ }
80
+ /**
81
+ * Compose the `canonicalUrl` a resolved `path` implies.
82
+ *
83
+ * TODO(ADR-040): compose canonicalUrl from OriginRegistryPort.canonicalOrigin(ctx)
84
+ * once `src/origin` exists, instead of `ctx.originOverride`. `src/origin` is
85
+ * being built in parallel by another agent and does not exist yet, so this
86
+ * library must not import it.
87
+ *
88
+ * @complexity O(1).
89
+ */
90
+ function composeCanonicalUrl(path, ctx) {
91
+ if (isAbsoluteUrl(path))
92
+ return path;
93
+ const origin = ctx.originOverride?.replace(/\/$/, "");
94
+ return origin ? `${origin}${path}` : path;
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Per-kind target resolvers (internal — dispatched by `urlFor`)
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * `published` is the only status that yields a live, linkable path (mirrors `getPublishedPostBySlug`,
101
+ * `post.ts`). Checks `isTrashed` too, not just `status` — `softDelete` (post.ts) stamps only
102
+ * `deletedAt`/`updatedAt`/`version`, never `status`, so a post that was `published` when trashed
103
+ * stays `status: "published"` forever, and `postRepo.findById` (this module's `resolveEntryRefTarget`
104
+ * and every other caller of {@link entryPublicPath}) is documented trash-BLIND. Without this check a
105
+ * trashed post would resolve to a live, linkable URL through `urlFor`/menu href resolution
106
+ * (`resolveStaticMenusForRender`, `server/inbound/public-http/routes/site/pages.ts`) even though the
107
+ * post itself 404s at every real render entrypoint. Same defect pattern as
108
+ * caa116115c103630ba5253f9bbe3bceb234347d8 (sitemap.xml/llms.txt) and a5c9bac8 (media rendition
109
+ * gating).
110
+ */
111
+ function isPublished(post) {
112
+ // Scheduled publishing (2026-10-05): a published row whose `publishAt` is still ahead gets no
113
+ // live link yet either — `isLiveAt` is `status === "published" && !isTrashed` plus that check.
114
+ return required.ports.isLive({ post });
115
+ }
116
+ /**
117
+ * Pure half of `entryRef` resolution: given a `PostRecord` already in hand, decides its public path
118
+ * with no repo call of its own. Factored out of {@link resolveEntryRefTarget} (and exported) so a
119
+ * caller that already holds the record — e.g. `features/post/tool-registrations.ts`'s
120
+ * `content_post_list`, iterating rows a prior `postRepo.list()` already fetched — can resolve
121
+ * `publicUrl` without a redundant `postRepo.findById` per row. `resolveEntryRefTarget`/`urlFor`
122
+ * still call this internally, so a caller resolving by id and a caller resolving from an
123
+ * already-held record always compute the identical path/canonicalUrl — the one-resolver property
124
+ * SEO's canonical tags depend on is unchanged.
125
+ *
126
+ * @returns `null` for a draft, unpublished, or otherwise non-live row — never a link a visitor
127
+ * would 404 on.
128
+ * @complexity O(1).
129
+ */
130
+ function entryPublicPath({ post, ctx }, _optional = {}) {
131
+ if (!isPublished(post))
132
+ return null;
133
+ const path = postPublicPath({ slug: post.slug });
134
+ return { path, canonicalUrl: composeCanonicalUrl(path, ctx) };
135
+ }
136
+ async function resolveEntryRefTarget(deps, target, ctx) {
137
+ const post = await deps.postRepo.findById({ workspaceId: ctx.workspaceId, id: target.entryId });
138
+ if (!post)
139
+ return null;
140
+ return entryPublicPath({ post: post, ctx: ctx });
141
+ }
142
+ /**
143
+ * Stub: taxonomy isn't implemented in this repo. ADR-029's Round-3 audit fold
144
+ * promoted "term-link schema" from an Open item to a Wave-1 acceptance
145
+ * blocker — ADR-022 §5's `entry_refs` covers entry-to-entry references only,
146
+ * with no term-target schema yet (content-lib sign-off on a typed `term_refs`
147
+ * index, or an accepted `entry_refs` extension for term targets, is owed).
148
+ * Always resolves `null` until that schema lands.
149
+ */
150
+ function resolveTermRefTarget(_target, _ctx) {
151
+ return null;
152
+ }
153
+ const UNSAFE_URL_SCHEMES = ["javascript:", "data:"];
154
+ /**
155
+ * @complexity O(1) (bounded scheme-prefix check).
156
+ */
157
+ function resolveUrlTarget(target, ctx) {
158
+ const href = target.href.trim();
159
+ const lowered = href.toLowerCase();
160
+ if (UNSAFE_URL_SCHEMES.some((scheme) => lowered.startsWith(scheme))) {
161
+ return null;
162
+ }
163
+ return { path: href, canonicalUrl: composeCanonicalUrl(href, ctx) };
164
+ }
165
+ function resolveRouteRefTarget(target, ctx) {
166
+ const path = getNamedRoute({ name: target.route });
167
+ if (path === null)
168
+ return null;
169
+ return { path, canonicalUrl: composeCanonicalUrl(path, ctx) };
170
+ }
171
+ /**
172
+ * Resolve a `RouteTarget` to its concrete path + canonical URL.
173
+ *
174
+ * Purpose: the one inverse-resolution entrypoint Menus and SEO depend on.
175
+ * Deviates from ADR-039 §2's 2-arg pseudocode (`urlFor(target, ctx)`) by
176
+ * taking a required-object first parameter carrying `deps` — `entryRef`
177
+ * resolution needs an async `PostRepoPort` call, so `urlFor` must be async and
178
+ * (per this repo's house style) DI'd rather than reaching for a singleton.
179
+ * The semantic contract (target + workspace-scoped ctx → `RouteUrl | null`) is
180
+ * unchanged.
181
+ *
182
+ * @returns `null` when the target is unavailable (not found, unpublished,
183
+ * unresolvable taxonomy term, unsafe URL scheme, or unregistered route name) —
184
+ * never throws for those cases, so callers can treat "no link" uniformly.
185
+ * @throws {RouteResolutionError} only if `target.kind` is not one of the four
186
+ * known discriminants (defensive; unreachable through the typed union).
187
+ * @complexity O(1) plus the cost of one repo call for `entryRef` targets.
188
+ */
189
+ async function urlFor(required, _optional = {}) {
190
+ const { deps, target, ctx } = required;
191
+ switch (target.kind) {
192
+ case "entryRef":
193
+ return resolveEntryRefTarget(deps, target, ctx);
194
+ case "termRef":
195
+ return resolveTermRefTarget(target, ctx);
196
+ case "url":
197
+ return resolveUrlTarget(target, ctx);
198
+ case "route":
199
+ return resolveRouteRefTarget(target, ctx);
200
+ default: {
201
+ const unreachable = target;
202
+ throw new RouteResolutionError(`unrecognized route target kind: ${JSON.stringify(unreachable)}`);
203
+ }
204
+ }
205
+ }
206
+ /**
207
+ * Whether `target` resolves to `currentPath` — the active-state Menus needs
208
+ * for nav highlighting.
209
+ *
210
+ * v0 does exact-path matching only, after normalization. Ancestor/prefix
211
+ * semantics for nested menu highlighting (is `/blog` active on `/blog/post`?)
212
+ * are explicitly deferred to v0.1 (ADR-039 "Open" item 3) since they also
213
+ * need the `urlFor → null` caller-render contract (hide vs. render inert)
214
+ * pinned first.
215
+ *
216
+ * @returns `false` when the target is unavailable (delegates to `urlFor`).
217
+ * @complexity Same as `urlFor` plus O(n) path normalization.
218
+ */
219
+ async function isActive(required, _optional = {}) {
220
+ const resolved = await urlFor({ deps: required.deps, target: required.target, ctx: required.ctx });
221
+ if (!resolved)
222
+ return false;
223
+ return normalizePath(resolved.path) === normalizePath(required.currentPath);
224
+ }
225
+ const phaseRegistry = {
226
+ pre_content: [],
227
+ post_content: [],
228
+ };
229
+ /** Every phase, so an owner-wide revoke never has to be told which phases it used. */
230
+ const ALL_PHASES = ["pre_content", "post_content"];
231
+ /**
232
+ * Register a resolver into an ordered, core-owned pipeline phase
233
+ * (`normalize → pre_content → content-resolve → post_content → 404`,
234
+ * ADR-039 §1). Not an open ADR-009 plugin hook in v1 — this is the seam
235
+ * Redirects (ADR-033) will register into once it exists; no real rules are
236
+ * registered by this library itself.
237
+ *
238
+ * Resolvers within a phase run in registration order; the first non-`null`
239
+ * outcome short-circuits the remaining resolvers in that phase and the rest
240
+ * of the pipeline. Per ADR-039 Round-2 amendment 5, a `pre_content` resolver
241
+ * must only ever return a redirect/gone decision with an audited rule — it
242
+ * must never fabricate a `not_found` for a path that live content could still
243
+ * resolve. This library does not enforce that invariant (no rules exist yet
244
+ * to violate it); it is a contract note for the future Redirects implementer.
245
+ *
246
+ * Lifecycle (2026-09-16). This registry is module-level and therefore
247
+ * process-wide, but its registrants are not: a composition root registers a
248
+ * handler that has CLOSED OVER that composition's resources, and more than one
249
+ * composition runs per process (an integration test boots one site per case,
250
+ * and `createRouteDeps()` plus `createSiteRouteDeps()` can both run in one
251
+ * test process). Append-only, that left every previous composition's closure
252
+ * live on the request path forever — benign
253
+ * while the superseded resources were in-memory, a `TypeError: The database
254
+ * connection is not open` 500 on every phase-running route once one of them
255
+ * was a site database that had since closed. Pass {@link
256
+ * RegisterResolvePhaseOptions.owner} to opt into supersede-on-re-registration;
257
+ * {@link unregisterResolvePhaseOwner} revokes an owner outright.
258
+ *
259
+ * @returns a disposer that removes exactly this registration (idempotent, and
260
+ * a no-op once an owner-scoped re-registration has already superseded it).
261
+ * @complexity O(1) for an unowned registration; O(n) in the phase's existing
262
+ * registrations for an owned one (it first evicts that owner's). O(1) space.
263
+ */
264
+ function registerResolvePhase({ phase, resolver }, options = {}) {
265
+ const owner = options.owner;
266
+ if (owner !== undefined) {
267
+ phaseRegistry[phase] = phaseRegistry[phase].filter((entry) => entry.owner !== owner);
268
+ }
269
+ const registration = { owner, handler: resolver, onError: options.onError ?? "fail" };
270
+ phaseRegistry[phase].push(registration);
271
+ return () => {
272
+ phaseRegistry[phase] = phaseRegistry[phase].filter((entry) => entry !== registration);
273
+ };
274
+ }
275
+ /**
276
+ * Revoke every handler `owner` registered, across every phase — the explicit
277
+ * teardown half of {@link RegisterResolvePhaseOptions.owner}, for a host that
278
+ * knows when a site is finished rather than only when the next one starts.
279
+ *
280
+ * Unowned registrations are never matched: `undefined` is the "no owner" marker,
281
+ * not an owner, so `unregisterResolvePhaseOwner(undefined)` is a deliberate no-op.
282
+ *
283
+ * @complexity O(n) in total registered handlers. O(1) space.
284
+ */
285
+ function unregisterResolvePhaseOwner({ owner }, _optional = {}) {
286
+ if (owner === undefined)
287
+ return;
288
+ for (const phase of ALL_PHASES) {
289
+ phaseRegistry[phase] = phaseRegistry[phase].filter((entry) => entry.owner !== owner);
290
+ }
291
+ }
292
+ /**
293
+ * Run one phase's handlers in registration order, first non-`null` outcome wins.
294
+ *
295
+ * A handler's fault policy is its own registration's {@link RegisterResolvePhaseOptions.onError}
296
+ * (2026-09-16, t91 F4.3 — previously every throw was unconditionally isolated). `"skip"` logs the
297
+ * failure and continues with the next handler, rather than propagating out of the phase — correct
298
+ * for a handler whose failure can only DECLINE an outcome (a redirect, a gone), never fabricate
299
+ * one, which is why Redirects opts into it (`redirects/phase-handler.ts`'s INV-03). `"fail"` (the
300
+ * default) rethrows instead: a handler that guards content (a takedown, a reserved path) must not
301
+ * silently wave a request through just because its own check broke. No log on the `"fail"` path —
302
+ * the caller's own catch reports it (`routes/site/pages.ts`'s `reportSiteRenderFault`), so a fault
303
+ * produces exactly one log line either way.
304
+ *
305
+ * The old universal-skip default is why a stale closed-database handler could take down `/` and
306
+ * `/pricing` while `/products` (which runs no phase) kept serving — that direction (never let one
307
+ * registrant 500 every route) is still correct for `"skip"`; it is just no longer assumed for
308
+ * every registrant.
309
+ *
310
+ * @complexity O(h) in the phase's registered handlers, plus each handler's own
311
+ * opaque cost; short-circuits on the first outcome or the first `"fail"`-policy throw.
312
+ */
313
+ async function runPhase(phase, path, ctx) {
314
+ // Snapshot: a handler is free to register or revoke during its own run, and
315
+ // mutating the live array mid-iteration would silently skip a sibling.
316
+ for (const { handler, onError } of [...phaseRegistry[phase]]) {
317
+ let outcome;
318
+ try {
319
+ outcome = await handler(path, ctx);
320
+ }
321
+ catch (err) {
322
+ if (onError !== "skip")
323
+ throw err;
324
+ // The host retains its operator-facing route fault channel.
325
+ required.ports.reportPhaseError({ message: `[routing] a ${phase} resolver failed for ${path} — skipping it`, error: err });
326
+ continue;
327
+ }
328
+ if (outcome)
329
+ return outcome;
330
+ }
331
+ return null;
332
+ }
333
+ /**
334
+ * Run the forward resolution pipeline for an inbound request path.
335
+ *
336
+ * v0 behavior: normalize the path, run `pre_content` resolvers, then (with no
337
+ * `content-resolve` step wired here — that is fixed core content lookup, out
338
+ * of scope for this library) run `post_content` resolvers. Falls through to
339
+ * `{ matched: false }` (404) if nothing resolves. This is a registration seam
340
+ * for Redirects, not full pipeline behavior — no real rules exist yet.
341
+ *
342
+ * @complexity O(h) where h is the total number of registered phase handlers
343
+ * that run before a match (or all of them, on a full miss); each handler's own
344
+ * cost is opaque to this function.
345
+ */
346
+ async function resolve(required, _optional = {}) {
347
+ const path = normalizePath(required.input.path);
348
+ const ctx = required.input.ctx;
349
+ const preOutcome = await runPhase("pre_content", path, ctx);
350
+ if (preOutcome)
351
+ return { matched: true, phase: "pre_content", outcome: preOutcome };
352
+ // content-resolve: fixed core content lookup, not a registered phase of this
353
+ // library in v0 — intentionally a no-op here (see file header).
354
+ const postOutcome = await runPhase("post_content", path, ctx);
355
+ if (postOutcome)
356
+ return { matched: true, phase: "post_content", outcome: postOutcome };
357
+ return { matched: false };
358
+ }
359
+ /**
360
+ * Run only the `pre_content` phase for `path` (ADR-PIPE-009 Decision B / C-012).
361
+ *
362
+ * The Decision B seam: `resolve()`'s v0 shape bundles `pre_content` and
363
+ * `post_content` back-to-back with no pause for a caller's own content
364
+ * lookup in between. This additive, thin wrapper around the already-tested
365
+ * module-private `runPhase()` lets a real site route run ONLY `pre_content`
366
+ * before its own content lookup, and (separately) `runPostContentPhase` only
367
+ * after that lookup has failed — exactly ADR-039 §1's documented pipeline
368
+ * order (`pre_content -> content-resolve -> post_content`), which the single
369
+ * `resolve()` export cannot reproduce when a real content lookup must run in
370
+ * the middle. `resolve()`'s own SHAPE is UNCHANGED by this addition — this
371
+ * paragraph is about these wrapper functions existing alongside it, not about
372
+ * `resolve()`'s own throw behavior. Separately (2026-09-16, t91 F4.3):
373
+ * `resolve()` now CAN reject when a `"fail"`-policy handler throws, exactly
374
+ * like these wrappers — both go through the same `runPhase()`, which gained
375
+ * that throw path in the same change. `resolve()` has no production caller
376
+ * today (grep; only tests call it), so this is a latent property, not a
377
+ * behavior change any caller currently observes.
378
+ *
379
+ * @complexity O(h) in registered `pre_content` handlers.
380
+ */
381
+ async function runPreContentPhase({ path, ctx }, _optional = {}) {
382
+ return runPhase("pre_content", normalizePath(path), ctx);
383
+ }
384
+ /**
385
+ * Run only the `post_content` phase for `path` (ADR-PIPE-009 Decision B / C-013).
386
+ * See {@link runPreContentPhase}'s doc for the full rationale.
387
+ *
388
+ * @complexity O(h) in registered `post_content` handlers.
389
+ */
390
+ async function runPostContentPhase({ path, ctx }, _optional = {}) {
391
+ return runPhase("post_content", normalizePath(path), ctx);
392
+ }
393
+ // ---------------------------------------------------------------------------
394
+ // SlugChangeCapture slot (ADR-039 §4)
395
+ // ---------------------------------------------------------------------------
396
+ let boundSlugChangeCapture;
397
+ /**
398
+ * Bind the single `SlugChangeCapture` implementation the content write
399
+ * chokepoint calls inside a rename transaction. Per ADR-039 Round-2
400
+ * amendment 1, no slot implementation may be plugin-supplied — this function
401
+ * has no plugin-facing registration path today, only a core composition-root
402
+ * call site.
403
+ *
404
+ * Fail-closed nuance (Round-2 amendment 3), NOT enforced here: an absent
405
+ * binding is only a safe no-op before Redirects is installed, or while
406
+ * slug-preservation is disabled for the workspace. Once link-preservation is
407
+ * enabled, a slug-changing rename with no bound capture MUST fail
408
+ * `LINK_PRESERVATION_UNAVAILABLE` rather than silently skip the redirect —
409
+ * that check belongs to the content write chokepoint (which knows whether
410
+ * preservation is enabled) and to Redirects (neither exists in this repo
411
+ * yet). This library only exposes the registration slot itself.
412
+ *
413
+ * @returns a disposer that unbinds exactly THIS registration: a no-op once a later call has bound
414
+ * another implementation, so a stale composition torn down after a newer one booted cannot clear
415
+ * the newer site's capture (the same identity scoping `registerResolvePhase`'s disposers use).
416
+ */
417
+ function registerSlugChangeCapture({ impl }, _optional = {}) {
418
+ boundSlugChangeCapture = impl;
419
+ return () => {
420
+ if (boundSlugChangeCapture === impl)
421
+ boundSlugChangeCapture = undefined;
422
+ };
423
+ }
424
+ /** The currently bound `SlugChangeCapture`, or `undefined` if none is bound. */
425
+ function getSlugChangeCapture(_required, _optional = {}) {
426
+ return boundSlugChangeCapture;
427
+ }
428
+ // ---------------------------------------------------------------------------
429
+ // Test-only registry resets (deep-imported directly by this library's own
430
+ // tests; deliberately NOT exported from index.ts's public barrel)
431
+ // ---------------------------------------------------------------------------
432
+ /** Restore all module-level registries to their v0 defaults. Test use only. */
433
+ function resetRoutingRegistrationsForTests(_required, _optional = {}) {
434
+ namedRoutes.clear();
435
+ for (const [name, path] of initialNamedRoutes)
436
+ namedRoutes.set(name, path);
437
+ phaseRegistry.pre_content = [];
438
+ phaseRegistry.post_content = [];
439
+ boundSlugChangeCapture = undefined;
440
+ }
441
+ return { entryPublicPath, getNamedRoute, getSlugChangeCapture, isActive, postPublicPath, registerNamedRoute, registerResolvePhase, registerSlugChangeCapture, resolve, runPostContentPhase, runPreContentPhase, unregisterResolvePhaseOwner, urlFor, resetRoutingRegistrationsForTests };
442
+ }
443
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @file Core type definitions for the Tovu `routing` Tier-2 library (ADR-039 v0).
3
+ *
4
+ * Purpose:
5
+ * Declares the `RouteTarget` vocabulary, the workspace-scoped resolve context,
6
+ * the inverse-resolver result shape, the forward resolution pipeline's phase
7
+ * vocabulary, and the `SlugChangeCapture` slot's payload shape. This is the one
8
+ * place `entryRef | termRef | url | route` is defined — promoted from
9
+ * Menus-local (ADR-029 §3) to routing-owned per ADR-039 §3, so Menus/SEO/
10
+ * Redirects share one vocabulary instead of inventing three.
11
+ *
12
+ * How it relates to the project:
13
+ * - `core/ports.ts` supplies the shared primitives (`UUID`, `ISODateTime`, …).
14
+ * - `routing/ports.ts` declares the dependency shape `urlFor`/`isActive` need
15
+ * (the existing `PostRepoPort`, reused rather than duplicated).
16
+ * - `routing/routing.ts` implements the resolve functions against these types.
17
+ *
18
+ * Architectural role:
19
+ * INTERFACES + TYPES ONLY (no feature logic). ADR-039 v0 is deliberately the
20
+ * minimal shape three other sections depend on; full permalink-structure
21
+ * design is explicitly deferred (ADR-039 "Open").
22
+ */
23
+ import type { JsonObject, UUID } from "@jini-ai/core/primitives";
24
+ /** Discriminant values for {@link RouteTarget}. */
25
+ export type RouteTargetKind = "entryRef" | "termRef" | "url" | "route";
26
+ /**
27
+ * A link to a content entry by id. Only `post` has a real resolver in this
28
+ * repo today (ADR-039 v0); `contentType` is accepted for forward-compat with
29
+ * future content types but is not required to resolve a post reference.
30
+ */
31
+ export interface EntryRefTarget {
32
+ readonly kind: "entryRef";
33
+ readonly entryId: UUID;
34
+ readonly contentType?: string;
35
+ }
36
+ /**
37
+ * A link to a taxonomy term (category/tag archive).
38
+ *
39
+ * NOT resolvable today: taxonomy isn't implemented in this repo, and ADR-029's
40
+ * Round-3 audit fold promoted "term-link schema" from an Open item to an
41
+ * explicit Wave-1 acceptance blocker — ADR-022 §5's `entry_refs` covers
42
+ * entry-to-entry references only, with no term-target schema yet (content-lib
43
+ * sign-off owed). `urlFor`/`isActive` resolve every `termRef` target to `null`
44
+ * until that schema lands; see the stub in `routing.ts`.
45
+ */
46
+ export interface TermRefTarget {
47
+ readonly kind: "termRef";
48
+ readonly termId: UUID;
49
+ readonly taxonomy: string;
50
+ }
51
+ /**
52
+ * An external or site-relative URL, passed through as given (scheme-validated:
53
+ * `javascript:`/`data:` are rejected and resolve to `null`, mirroring the
54
+ * "unavailable → null" contract rather than throwing).
55
+ */
56
+ export interface UrlTarget {
57
+ readonly kind: "url";
58
+ readonly href: string;
59
+ }
60
+ /** A named core route (e.g. `home`, `search`), resolved via the route registry. */
61
+ export interface RouteRefTarget {
62
+ readonly kind: "route";
63
+ readonly route: string;
64
+ /** Optional static params; unused by the v0 registry, reserved for v0.1 param interpolation. */
65
+ readonly params?: JsonObject;
66
+ }
67
+ /** v0 discriminated union of route targets (ADR-039 §3). */
68
+ export type RouteTarget = EntryRefTarget | TermRefTarget | UrlTarget | RouteRefTarget;
69
+ /**
70
+ * Workspace-scoped context threaded through every resolve call. `siteId`/
71
+ * `locale`/`originKey` are reserved for multi-site/i18n v0.1 work (ADR-039
72
+ * "Open"); only `workspaceId` is load-bearing today.
73
+ *
74
+ * `originOverride` is a stand-in for `core/origin` (ADR-040), which is being
75
+ * built in parallel and does not exist yet. Once it lands, `canonicalUrl`
76
+ * composition should read the verified origin from
77
+ * `OriginRegistryPort.canonicalOrigin(ctx)` instead of this field.
78
+ */
79
+ export interface RouteResolveContext {
80
+ readonly workspaceId: UUID;
81
+ readonly siteId?: string;
82
+ readonly locale?: string;
83
+ readonly originKey?: string;
84
+ /**
85
+ * TODO(ADR-040): compose canonicalUrl from OriginRegistryPort.canonicalOrigin(ctx)
86
+ * once src/origin exists. Until then, a caller that knows the verified origin
87
+ * may pass it here; absent, canonicalUrl falls back to the relative path.
88
+ */
89
+ readonly originOverride?: string;
90
+ }
91
+ /** Result of a successful inverse resolve — the shape Menus/SEO consume. */
92
+ export interface RouteUrl {
93
+ readonly path: string;
94
+ readonly canonicalUrl: string;
95
+ }
96
+ /**
97
+ * The two registerable phases of the forward pipeline
98
+ * (`normalize → pre_content → content-resolve → post_content → 404`).
99
+ * `content-resolve` is fixed core content lookup and is not a registerable
100
+ * phase of this library; it is out of scope for ADR-039 v0's registration
101
+ * mechanism.
102
+ */
103
+ export type RouteResolvePhaseName = "pre_content" | "post_content";
104
+ /** Terminal decision a phase resolver may return; anything else falls through. */
105
+ export type RouteResolvePhaseOutcome = {
106
+ readonly kind: "redirect";
107
+ readonly location: string;
108
+ readonly statusCode: number;
109
+ } | {
110
+ readonly kind: "resolved";
111
+ readonly path: string;
112
+ } | {
113
+ readonly kind: "not_found";
114
+ };
115
+ /**
116
+ * A single phase resolver. Returns `null` to fall through to the next
117
+ * registered resolver in the same phase (and eventually the next phase);
118
+ * returns an outcome to short-circuit the pipeline (ADR-039 §1 invariant:
119
+ * `pre_content` may only redirect/gone, never turn a resolvable live path
120
+ * into a 404 — Round-2 amendment 5).
121
+ */
122
+ export type RouteResolvePhaseHandler = (path: string, ctx: RouteResolveContext) => Promise<RouteResolvePhaseOutcome | null>;
123
+ /** Final outcome of {@link resolve} after all phases have run. */
124
+ export type RouteResolveResult = {
125
+ readonly matched: false;
126
+ } | {
127
+ readonly matched: true;
128
+ readonly phase: RouteResolvePhaseName;
129
+ readonly outcome: RouteResolvePhaseOutcome;
130
+ };
131
+ /** Payload handed to the bound {@link SlugChangeCapture} inside the rename transaction. */
132
+ export interface SlugChangeCaptureInput {
133
+ readonly workspaceId: UUID;
134
+ readonly entryId: UUID;
135
+ readonly oldPath: string;
136
+ readonly newPath: string;
137
+ readonly actor: UUID;
138
+ readonly changeSetId?: UUID;
139
+ }
140
+ /**
141
+ * The single named slot content's write chokepoint calls inside the rename
142
+ * transaction (ADR-039 §4). Declared here by routing (content knows nothing of
143
+ * redirects); implemented by the redirects library; wired by the composition
144
+ * root. **No slot implementation may be plugin-supplied** (Round-2 amendment 1
145
+ * — the slot is core-resident by definition).
146
+ *
147
+ * Constraints on any bound implementation (Round-2 amendment 2): idempotent by
148
+ * `changeSetId`, no external I/O, no in-tx publish (an outbox row may be
149
+ * written in-tx per the transactional-outbox pattern, but dispatch happens
150
+ * post-commit), and a strict lock order of content row before redirect rows.
151
+ *
152
+ * Fail-closed nuance (Round-2 amendment 3, not enforced by this library):
153
+ * absent binding is a no-op only before Redirects is installed, or while
154
+ * slug-preservation is disabled for the workspace. Once link-preservation is
155
+ * enabled, a slug-changing rename with no bound capture MUST fail
156
+ * `LINK_PRESERVATION_UNAVAILABLE` rather than silently skip the redirect. That
157
+ * enforcement belongs to the content write chokepoint (which knows whether
158
+ * preservation is enabled) and to Redirects (which doesn't exist yet in this
159
+ * repo) — this library only exposes the registration slot.
160
+ */
161
+ export interface SlugChangeCapture {
162
+ onSlugChange(input: SlugChangeCaptureInput): Promise<void>;
163
+ }
164
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -29,6 +29,29 @@ export declare function resolveSeoImageRef(required: {
29
29
  deps: ResolveSeoImageRefDeps;
30
30
  input: ResolveSeoImageRefInput;
31
31
  }, _optional?: Record<string, never>): Promise<string | undefined>;
32
+ /** Metadata belongs to the selected transform, never the asset's editable display-size fields. */
33
+ export interface SeoImageDetails {
34
+ url: string;
35
+ /** Original source choice for admin diagnostics; a resized rendition can conceal a tiny source. */
36
+ ref?: string | undefined;
37
+ width?: number | undefined;
38
+ height?: number | undefined;
39
+ type?: string | undefined;
40
+ }
41
+ /** Resolve through the existing media owners, optionally selecting a host's share transform.
42
+ * Absolute external URLs remain unchanged with unknown metadata; no remote fetch occurs.
43
+ * The latest definition keeps the anonymous lazy-generation guarantee documented above.
44
+ * @param required Media ports, workspace and persisted reference.
45
+ * @param optional Host-selected transform; omitted or unregistered retains the reference's named transform.
46
+ * @returns URL and known output metadata, or undefined on a lookup miss.
47
+ * @complexity O(v) in registered transform versions; up to three bounded repository reads.
48
+ */
49
+ export declare function resolveSeoImageDetails(required: {
50
+ deps: ResolveSeoImageRefDeps;
51
+ input: ResolveSeoImageRefInput;
52
+ }, optional?: {
53
+ transformName?: string | undefined;
54
+ }): Promise<SeoImageDetails | undefined>;
32
55
  /**
33
56
  * The write-path refusal for an `ogImage`/`twitterImage`/`defaultOgImage` value (2026-10-05): the
34
57
  * message to refuse it with, or `null` to accept it. Both SEO write chokepoints call it