@jini-ai/cms 0.5.1 → 0.5.3

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 (209) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/media/index.d.ts +1 -2
  46. package/dist/media/index.js +2 -2
  47. package/dist/media/node.d.ts +5 -0
  48. package/dist/media/node.js +6 -0
  49. package/dist/media/video-frames.d.ts +94 -0
  50. package/dist/media/video-frames.ffmpeg.d.ts +39 -0
  51. package/dist/media/video-frames.ffmpeg.js +261 -0
  52. package/dist/media/video-frames.js +97 -0
  53. package/dist/redirects/capture.d.ts +80 -0
  54. package/dist/redirects/capture.js +66 -0
  55. package/dist/redirects/express/index.d.ts +27 -0
  56. package/dist/redirects/express/index.js +40 -0
  57. package/dist/redirects/hit-sink.d.ts +56 -0
  58. package/dist/redirects/hit-sink.js +44 -0
  59. package/dist/redirects/host-config/index.d.ts +40 -0
  60. package/dist/redirects/host-config/index.js +145 -0
  61. package/dist/redirects/index.d.ts +14 -0
  62. package/dist/redirects/index.js +14 -0
  63. package/dist/redirects/matcher.d.ts +58 -0
  64. package/dist/redirects/matcher.js +144 -0
  65. package/dist/redirects/order.d.ts +30 -0
  66. package/dist/redirects/order.js +26 -0
  67. package/dist/redirects/ports.d.ts +242 -0
  68. package/dist/redirects/ports.internal.d.ts +57 -0
  69. package/dist/redirects/ports.internal.js +14 -0
  70. package/dist/redirects/ports.js +2 -0
  71. package/dist/redirects/redirects.d.ts +226 -0
  72. package/dist/redirects/redirects.js +590 -0
  73. package/dist/redirects/referrer-alias.d.ts +31 -0
  74. package/dist/redirects/referrer-alias.js +33 -0
  75. package/dist/redirects/repo.memory.d.ts +51 -0
  76. package/dist/redirects/repo.memory.js +98 -0
  77. package/dist/redirects/reserved-destination.d.ts +58 -0
  78. package/dist/redirects/reserved-destination.js +59 -0
  79. package/dist/redirects/resolver.d.ts +100 -0
  80. package/dist/redirects/resolver.js +87 -0
  81. package/dist/redirects/spa/index.d.ts +30 -0
  82. package/dist/redirects/spa/index.js +27 -0
  83. package/dist/redirects/sql/index.d.ts +4 -0
  84. package/dist/redirects/sql/index.js +4 -0
  85. package/dist/redirects/sql/repo.d.ts +82 -0
  86. package/dist/redirects/sql/repo.js +143 -0
  87. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  88. package/dist/redirects/sql/repo.rows.js +79 -0
  89. package/dist/redirects/sql/row-types.d.ts +34 -0
  90. package/dist/redirects/sql/row-types.js +2 -0
  91. package/dist/redirects/target-gate.d.ts +30 -0
  92. package/dist/redirects/target-gate.js +85 -0
  93. package/dist/redirects/types.d.ts +213 -0
  94. package/dist/redirects/types.js +42 -0
  95. package/dist/seo/absolute-url.d.ts +58 -0
  96. package/dist/seo/absolute-url.js +117 -0
  97. package/dist/seo/dom/index.d.ts +14 -0
  98. package/dist/seo/dom/index.js +52 -0
  99. package/dist/seo/errors.d.ts +35 -0
  100. package/dist/seo/errors.js +35 -0
  101. package/dist/seo/feed.d.ts +49 -0
  102. package/dist/seo/feed.js +61 -0
  103. package/dist/seo/index.d.ts +13 -0
  104. package/dist/seo/index.js +13 -0
  105. package/dist/seo/media.d.ts +57 -0
  106. package/dist/seo/media.js +134 -0
  107. package/dist/seo/page-head.d.ts +18 -0
  108. package/dist/seo/page-head.js +114 -0
  109. package/dist/seo/ports.d.ts +221 -0
  110. package/dist/seo/ports.js +27 -0
  111. package/dist/seo/seo.d.ts +73 -0
  112. package/dist/seo/seo.js +230 -0
  113. package/dist/seo/sitemap.d.ts +139 -0
  114. package/dist/seo/sitemap.js +217 -0
  115. package/dist/seo/types.d.ts +278 -0
  116. package/dist/seo/types.js +2 -0
  117. package/dist/seo/write-service.d.ts +79 -0
  118. package/dist/seo/write-service.js +273 -0
  119. package/dist/seo/xml-escape.d.ts +16 -0
  120. package/dist/seo/xml-escape.js +19 -0
  121. package/dist/settings/express/cms-adapter.d.ts +20 -0
  122. package/dist/settings/express/cms-adapter.js +124 -0
  123. package/dist/settings/express/contracts.d.ts +135 -0
  124. package/dist/settings/express/contracts.js +2 -0
  125. package/dist/settings/express/http.d.ts +10 -0
  126. package/dist/settings/express/http.js +211 -0
  127. package/dist/settings/express/index.d.ts +5 -0
  128. package/dist/settings/express/index.js +5 -0
  129. package/dist/settings/express/shared.d.ts +68 -0
  130. package/dist/settings/express/shared.js +54 -0
  131. package/dist/settings/express/stream.d.ts +24 -0
  132. package/dist/settings/express/stream.js +147 -0
  133. package/dist/settings/index.d.ts +3 -38
  134. package/dist/settings/index.js +3 -35
  135. package/dist/taxonomy/write-service.d.ts +2 -30
  136. package/dist/widgets/authorize-helper.d.ts +42 -0
  137. package/dist/widgets/authorize-helper.js +16 -0
  138. package/dist/widgets/concurrency.d.ts +12 -0
  139. package/dist/widgets/concurrency.js +44 -0
  140. package/dist/widgets/config-validation.d.ts +34 -0
  141. package/dist/widgets/config-validation.js +125 -0
  142. package/dist/widgets/embed-service.d.ts +133 -0
  143. package/dist/widgets/embed-service.js +460 -0
  144. package/dist/widgets/embed-validation.d.ts +23 -0
  145. package/dist/widgets/embed-validation.js +38 -0
  146. package/dist/widgets/entry-payload.d.ts +103 -0
  147. package/dist/widgets/entry-payload.js +160 -0
  148. package/dist/widgets/errors.d.ts +136 -0
  149. package/dist/widgets/errors.js +136 -0
  150. package/dist/widgets/host-ports.d.ts +149 -0
  151. package/dist/widgets/host-ports.js +2 -0
  152. package/dist/widgets/html/html-embeds.d.ts +247 -0
  153. package/dist/widgets/html/html-embeds.js +408 -0
  154. package/dist/widgets/html/index.d.ts +5 -0
  155. package/dist/widgets/html/index.js +4 -0
  156. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  157. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  158. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  159. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  160. package/dist/widgets/index.d.ts +20 -0
  161. package/dist/widgets/index.js +19 -0
  162. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  163. package/dist/widgets/markers/html-attributes.js +216 -0
  164. package/dist/widgets/markers/index.d.ts +3 -0
  165. package/dist/widgets/markers/index.js +3 -0
  166. package/dist/widgets/markers/marker.d.ts +496 -0
  167. package/dist/widgets/markers/marker.js +780 -0
  168. package/dist/widgets/page-embed-types.d.ts +42 -0
  169. package/dist/widgets/page-embed-types.js +42 -0
  170. package/dist/widgets/ports.d.ts +142 -0
  171. package/dist/widgets/ports.js +2 -0
  172. package/dist/widgets/read-service.d.ts +81 -0
  173. package/dist/widgets/read-service.js +79 -0
  174. package/dist/widgets/region-area-service.d.ts +110 -0
  175. package/dist/widgets/region-area-service.js +180 -0
  176. package/dist/widgets/registry.d.ts +217 -0
  177. package/dist/widgets/registry.js +172 -0
  178. package/dist/widgets/repo.memory.d.ts +41 -0
  179. package/dist/widgets/repo.memory.js +48 -0
  180. package/dist/widgets/resolver-service.d.ts +209 -0
  181. package/dist/widgets/resolver-service.js +909 -0
  182. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  183. package/dist/widgets/resolvers/contact-form.js +54 -0
  184. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  185. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  186. package/dist/widgets/resolvers/index.d.ts +65 -0
  187. package/dist/widgets/resolvers/index.js +155 -0
  188. package/dist/widgets/resolvers/menu.d.ts +42 -0
  189. package/dist/widgets/resolvers/menu.js +43 -0
  190. package/dist/widgets/resolvers/ports.d.ts +82 -0
  191. package/dist/widgets/resolvers/ports.js +2 -0
  192. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  193. package/dist/widgets/resolvers/recent-entries.js +165 -0
  194. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  195. package/dist/widgets/resolvers/taxonomy.js +34 -0
  196. package/dist/widgets/sql/index.d.ts +3 -0
  197. package/dist/widgets/sql/index.js +3 -0
  198. package/dist/widgets/sql/repo.d.ts +41 -0
  199. package/dist/widgets/sql/repo.js +82 -0
  200. package/dist/widgets/sql/tables.d.ts +13 -0
  201. package/dist/widgets/sql/tables.js +2 -0
  202. package/dist/widgets/types.d.ts +213 -0
  203. package/dist/widgets/types.js +17 -0
  204. package/dist/widgets/where-used.d.ts +41 -0
  205. package/dist/widgets/where-used.js +11 -0
  206. package/dist/widgets/write-service.d.ts +204 -0
  207. package/dist/widgets/write-service.js +332 -0
  208. package/dist/workspace/delete.d.ts +1 -1
  209. package/package.json +220 -11
@@ -0,0 +1,26 @@
1
+ import type { FormDefinitionReadPort } from "./ports.js";
2
+ import type { WidgetResolver } from "../types.js";
3
+ /**
4
+ * @file `contact-form` widget resolver (SPEC-043 REQ-36..39, ADR-047 Debate Fold-In Amendment 4).
5
+ *
6
+ * Purpose:
7
+ * A thin, READ-ONLY adapter over `src/forms/`'s definition read side. Imports ONLY the read-only
8
+ * `FormDefinitionRepoPort` type, through `forms/index.ts` (ADR-009 §1's barrel; the type is
9
+ * declared in `forms/ports.ts`) — never the Forms library's own submission write path or its
10
+ * outbox-driven mail-notification module — so this file can never persist a submission, send
11
+ * mail, rate-limit, or duplicate any part of the Forms pipeline (INV-08, hard invariant; verified
12
+ * by a code-review-level grep over this whole package for those two module names, which must
13
+ * return nothing).
14
+ *
15
+ * REQ-37 ("reads the referenced Forms definition's declared field vocabulary — never a hardcoded
16
+ * field-type list") is honored here: `definition.fields` (Forms' own `FieldDescriptor[]`) is passed
17
+ * through as IR props verbatim, not re-enumerated against a widget-local field-kind switch. The
18
+ * actual submission still posts to Forms' existing public route unmodified (REQ-37's second half) —
19
+ * that is a render-component/route concern, out of this resolver's scope (no HTTP route exists yet
20
+ * for widgets in this slice; see the implementation report's scope-boundary notes).
21
+ */
22
+ export interface ContactFormResolverDeps {
23
+ formDefinitionRepo: FormDefinitionReadPort;
24
+ }
25
+ export declare function createContactFormResolver(deps: ContactFormResolverDeps, _optional?: {}): WidgetResolver;
26
+ //# sourceMappingURL=contact-form.d.ts.map
@@ -0,0 +1,54 @@
1
+ export function createContactFormResolver(deps, _optional = {}) {
2
+ return {
3
+ async resolveMany(instances, context) {
4
+ const results = new Map();
5
+ for (const instance of instances) {
6
+ const formDefinitionId = typeof instance.config.formDefinitionId === "string" ? instance.config.formDefinitionId : undefined;
7
+ const formDefinitionRef = typeof instance.config.formDefinitionRef === "string" ? instance.config.formDefinitionRef : undefined;
8
+ if (!formDefinitionId && !formDefinitionRef) {
9
+ results.set(instance.id, { ok: false, reason: "invalid-config" });
10
+ continue;
11
+ }
12
+ const byId = await deps.formDefinitionRepo.findById({
13
+ workspaceId: context.workspaceId,
14
+ id: formDefinitionId ?? formDefinitionRef,
15
+ });
16
+ const definition = byId ?? (formDefinitionRef ? await deps.formDefinitionRepo.findBySlug({
17
+ workspaceId: context.workspaceId, slug: formDefinitionRef,
18
+ }) : null);
19
+ // REQ-38/EC-05: a disabled, trashed (`findById` hides it) or purged definition is the same
20
+ // failure-isolation placeholder, never an error.
21
+ if (!definition || definition.status !== "active") {
22
+ results.set(instance.id, { ok: false, reason: "target-disabled" });
23
+ continue;
24
+ }
25
+ results.set(instance.id, {
26
+ ok: true,
27
+ ir: {
28
+ componentId: "contact-form",
29
+ props: {
30
+ formDefinitionId: definition.id,
31
+ // Closes this file's own previously-disclosed deferral ("the actual submission still
32
+ // posts to Forms' existing public route unmodified... that is a render-component/route
33
+ // concern, out of this resolver's scope") — the render component built for SPEC-043's
34
+ // routes/UI slice (`server/http/site/render.ts`'s `renderWidgetContactForm`) needs the
35
+ // definition's `slug` to build `POST /forms/:slug/submit`'s URL (REQ-37's second half).
36
+ slug: definition.slug,
37
+ // REQ-37: Forms' own declared field vocabulary, passed through verbatim — never a
38
+ // hardcoded field-type list. Cast: FieldDescriptor[] has no index signature of its
39
+ // own, but every field is plain JSON-serializable data (SPEC-010 forms/types.ts).
40
+ fields: definition.fields,
41
+ ...(definition.mode === "html" ? {
42
+ mode: "html", html: definition.html ?? "",
43
+ } : {}),
44
+ successMessage: typeof instance.config.successMessage === "string" ? instance.config.successMessage : null,
45
+ },
46
+ },
47
+ dependencyKeys: [definition.id],
48
+ });
49
+ }
50
+ return results;
51
+ },
52
+ };
53
+ }
54
+ //# sourceMappingURL=contact-form.js.map
@@ -0,0 +1,34 @@
1
+ import type { EntryDisplayListPort, EntryListExcludingTypesPort, CollectionListPort, MenusPort, FormDefinitionReadPort } from "./ports.js";
2
+ import type { EntryListPort } from "../../entries/index.js";
3
+ import type { NavMenuReadModel, ResolveTargetHrefFn } from "../../navigation/index.js";
4
+ import type { WidgetResolver, WidgetTypeKey } from "../types.js";
5
+ import { type ContentTypeLookup } from "./recent-entries.js";
6
+ /**
7
+ * @file Assembles the real, DI'd v1 dynamic resolvers against real infrastructure deps.
8
+ *
9
+ * Purpose:
10
+ * The one place that turns "real repo adapters" into "the closed `CORE_RESOLVERS` map"
11
+ * (`resolvers/index.ts`'s `wireCoreResolvers` calls this). Kept separate from `index.ts` itself so
12
+ * the dispatch module (`resolveWidgetType`, exercised by every test in this slice) has no
13
+ * import-time dependency on `features/entries`/`navigation`/`forms` deps that a pure dispatch unit
14
+ * test would otherwise need to construct.
15
+ */
16
+ export interface CoreResolverDeps {
17
+ menus: MenusPort;
18
+ collectionList: CollectionListPort;
19
+ resolveMenuTargetHref?: ResolveTargetHrefFn;
20
+ /** Widened by collections plan R1 with `EntryDisplayListPort` — the `recent-entries` widget's
21
+ * "Collection list" mode shares `EntryDisplayListPort.listPublishedForDisplay` with the
22
+ * `{"type":"collection"}` marker (C2). Widened again by review fix 3b with
23
+ * `EntryListExcludingTypesPort`, the legacy (no-`collection`) path's query-level
24
+ * `SYSTEM_CONTENT_TYPES` exclusion. Both composition roots' real `entryRepo` already implements
25
+ * all three (`server/runtime/composition/{app,deps}.ts`'s `TrashAwareInMemoryEntryRepo`/`SqliteEntryRepo`). */
26
+ entryList: EntryListPort & EntryDisplayListPort & EntryListExcludingTypesPort;
27
+ navMenuReadModel: NavMenuReadModel;
28
+ formDefinitionRepo: FormDefinitionReadPort;
29
+ /** R1 addition — resolves a `collection` config's target content type. See `recent-entries.ts`'s
30
+ * `ContentTypeLookup` for why this is its own narrow port rather than `ContentTypeRepoPort`. */
31
+ contentTypes: ContentTypeLookup;
32
+ }
33
+ export declare function createCoreResolvers(deps: CoreResolverDeps, _optional?: Record<string, never>): Partial<Record<WidgetTypeKey, WidgetResolver>>;
34
+ //# sourceMappingURL=create-core-resolvers.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { createContactFormResolver } from "./contact-form.js";
2
+ import { createMenuResolver } from "./menu.js";
3
+ import { createRecentEntriesResolver } from "./recent-entries.js";
4
+ export function createCoreResolvers(deps, _optional = {}) {
5
+ return {
6
+ "recent-entries": createRecentEntriesResolver({ collectionList: deps.collectionList, entryList: deps.entryList, contentTypes: deps.contentTypes }),
7
+ menu: createMenuResolver({ menus: deps.menus, navMenuReadModel: deps.navMenuReadModel, ...(deps.resolveMenuTargetHref === undefined ? {} : { resolveTargetHref: deps.resolveMenuTargetHref }), publicOnly: true }),
8
+ "contact-form": createContactFormResolver({ formDefinitionRepo: deps.formDefinitionRepo }),
9
+ };
10
+ }
11
+ //# sourceMappingURL=create-core-resolvers.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @file `CORE_RESOLVERS` — the closed, core-owned resolver dispatch map (SPEC-043 REQ-08/09/10/23;
3
+ * ADR-047 Debate Fold-In Amendment 2/3).
4
+ *
5
+ * Purpose:
6
+ * The ONE place a registry's `resolverId` string resolves against real, executable code. A
7
+ * `WidgetTypeRegistration.resolverId` is never used as a dynamic import path, `eval`-style
8
+ * reference, or arbitrary function lookup anywhere else in this codebase (REQ-08) — this map is
9
+ * the sole allowlist. `resolveWidgetType` is the dispatcher every page render goes through
10
+ * (`resolver-service.ts`'s `resolvePageWidgets` calls this, once per distinct type present),
11
+ * and it owns the try/catch + timeout boundary so a resolver's exception or hang can never
12
+ * propagate past a widget's placement boundary (REQ-27, INV-05).
13
+ *
14
+ * Architectural role:
15
+ * TDD-certified implementation (implementation outline C-003). The dispatcher's failure-isolation
16
+ * contract is design-frozen from SPEC-043; `resolveWidgetType()` dispatches to the closed
17
+ * `CORE_RESOLVERS` map (or the static path for a resolver-less type), wrapping every call in the
18
+ * shared try/catch + timeout boundary so a resolver failure never propagates past REQ-27/INV-05.
19
+ * Verified against `__tests__/integration/resolver-service.integration.test.ts`.
20
+ */
21
+ import type { UUID } from "@jini-ai/core/primitives";
22
+ import { type CoreResolverDeps } from "./create-core-resolvers.js";
23
+ import type { WidgetInstanceView, WidgetResolveContext, WidgetResolveResult, WidgetResolver, WidgetTypeKey } from "../types.js";
24
+ export { createRecentEntriesResolver } from "./recent-entries.js";
25
+ export { createMenuResolver } from "./menu.js";
26
+ export { createContactFormResolver } from "./contact-form.js";
27
+ /**
28
+ * The closed map. Adding a resolver = adding one entry here, never a dynamic import. Types with
29
+ * capability `static` (no `resolverId`) never appear here — `resolveWidgetType` takes the static
30
+ * path for those instead of consulting this map.
31
+ *
32
+ * Typed `Readonly<...>` so every ordinary caller gets a compile-time guarantee this map is never
33
+ * mutated ad hoc from outside `registerCoreResolver`/`wireCoreResolvers` — the certified test
34
+ * suite's direct-assignment test-double seam (`CORE_RESOLVERS["recent-entries"] = resolver`)
35
+ * deliberately overrides that guarantee with `@ts-expect-error`, since at runtime this is the same
36
+ * plain, mutable object `mutableResolvers` is (TypeScript's `readonly` has no runtime effect) —
37
+ * this is intentional, not a bug: it lets the test suite swap in failure-injecting test doubles
38
+ * without a second, parallel test-only export.
39
+ */
40
+ export declare const CORE_RESOLVERS: Readonly<Partial<Record<WidgetTypeKey, WidgetResolver>>>;
41
+ /** Registers (or replaces) one resolver in the closed map — the one typed, non-test way to populate it. */
42
+ export declare function registerCoreResolver(required: {
43
+ typeKey: WidgetTypeKey;
44
+ resolver: WidgetResolver;
45
+ }, _optional?: Record<string, never>): void;
46
+ /**
47
+ * Assembles and registers every real, DI'd v1 dynamic resolver (`recent-entries`, `menu`,
48
+ * `contact-form` — see the sibling files in this directory) against the given infrastructure
49
+ * deps. Not called automatically at module load (these resolvers need injected repos that don't
50
+ * exist yet at pure import time — see each resolver factory's own doc) — a future boot-wiring
51
+ * pass (`server/app.ts`/`server/deps.ts`, out of this task's scope) is expected to call this once,
52
+ * with real adapters, before the app serves traffic. Left uncalled here is a disclosed gap, not an
53
+ * oversight — no test in this slice exercises real dynamic-resolver behavior (see
54
+ * `resolvers/{recent-entries,menu,contact-form}.ts`'s own file headers).
55
+ */
56
+ export declare function wireCoreResolvers(deps: CoreResolverDeps, _optional?: Record<string, never>): void;
57
+ export declare function resolveWidgetType(required: {
58
+ typeKey: WidgetTypeKey;
59
+ instances: readonly WidgetInstanceView[];
60
+ context: WidgetResolveContext;
61
+ }, _optional?: Record<string, never>): Promise<ReadonlyMap<UUID, WidgetResolveResult>>;
62
+ export * from "./ports.js";
63
+ export { createCoreResolvers, type CoreResolverDeps } from "./create-core-resolvers.js";
64
+ export { resolveTaxonomyEmbeds } from "./taxonomy.js";
65
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { findWidgetTypeRegistration } from "../registry.js";
2
+ import { createCoreResolvers } from "./create-core-resolvers.js";
3
+ // `create-core-resolvers.ts` is the only production caller of the three factories below — they're
4
+ // wired into `CORE_RESOLVERS` via `wireCoreResolvers`, never invoked directly outside this module.
5
+ // Each also has its own dedicated unit/integration test exercising the resolver's own logic in
6
+ // isolation (2026-08-18 widgets/resolvers deep-import triage), so each factory is re-exported here
7
+ // narrowly, one line per consumer:
8
+ // `createRecentEntriesResolver` — `widgets/__tests__/unit/resolvers-recent-entries.unit.test.ts`.
9
+ export { createRecentEntriesResolver } from "./recent-entries.js";
10
+ // `createMenuResolver` — `widgets/__tests__/unit/resolvers-menu.unit.test.ts`.
11
+ export { createMenuResolver } from "./menu.js";
12
+ // `createContactFormResolver` — `widgets/__tests__/integration/resolve-html-page-embeds.integration.test.ts`.
13
+ export { createContactFormResolver } from "./contact-form.js";
14
+ /**
15
+ * The mutable backing store `CORE_RESOLVERS` (below) exposes only a readonly view over — see that
16
+ * export's own doc for why the map itself must stay assignable at runtime despite its readonly TS
17
+ * type (the certified test suite mutates it directly, via `@ts-expect-error`, as its test-double
18
+ * seam). `registerCoreResolver`/`wireCoreResolvers` are the two real, typed ways production code
19
+ * populates it; nothing else in this module writes to it.
20
+ */
21
+ const mutableResolvers = {};
22
+ /**
23
+ * The closed map. Adding a resolver = adding one entry here, never a dynamic import. Types with
24
+ * capability `static` (no `resolverId`) never appear here — `resolveWidgetType` takes the static
25
+ * path for those instead of consulting this map.
26
+ *
27
+ * Typed `Readonly<...>` so every ordinary caller gets a compile-time guarantee this map is never
28
+ * mutated ad hoc from outside `registerCoreResolver`/`wireCoreResolvers` — the certified test
29
+ * suite's direct-assignment test-double seam (`CORE_RESOLVERS["recent-entries"] = resolver`)
30
+ * deliberately overrides that guarantee with `@ts-expect-error`, since at runtime this is the same
31
+ * plain, mutable object `mutableResolvers` is (TypeScript's `readonly` has no runtime effect) —
32
+ * this is intentional, not a bug: it lets the test suite swap in failure-injecting test doubles
33
+ * without a second, parallel test-only export.
34
+ */
35
+ export const CORE_RESOLVERS = mutableResolvers;
36
+ /** Registers (or replaces) one resolver in the closed map — the one typed, non-test way to populate it. */
37
+ export function registerCoreResolver(required, _optional = {}) {
38
+ mutableResolvers[required.typeKey] = required.resolver;
39
+ }
40
+ /**
41
+ * Assembles and registers every real, DI'd v1 dynamic resolver (`recent-entries`, `menu`,
42
+ * `contact-form` — see the sibling files in this directory) against the given infrastructure
43
+ * deps. Not called automatically at module load (these resolvers need injected repos that don't
44
+ * exist yet at pure import time — see each resolver factory's own doc) — a future boot-wiring
45
+ * pass (`server/app.ts`/`server/deps.ts`, out of this task's scope) is expected to call this once,
46
+ * with real adapters, before the app serves traffic. Left uncalled here is a disclosed gap, not an
47
+ * oversight — no test in this slice exercises real dynamic-resolver behavior (see
48
+ * `resolvers/{recent-entries,menu,contact-form}.ts`'s own file headers).
49
+ */
50
+ export function wireCoreResolvers(deps, _optional = {}) {
51
+ for (const [typeKey, resolver] of Object.entries(createCoreResolvers(deps))) {
52
+ registerCoreResolver({ typeKey, resolver });
53
+ }
54
+ }
55
+ /** Converts a resolver exception/hang into a typed failure — never propagates (REQ-27, INV-05). */
56
+ class ResolverTimeoutError extends Error {
57
+ }
58
+ async function withResolverTimeout(promise, timeoutMs) {
59
+ if (!timeoutMs || timeoutMs <= 0)
60
+ return promise;
61
+ return new Promise((resolve, reject) => {
62
+ const timer = setTimeout(() => reject(new ResolverTimeoutError(`resolver exceeded ${timeoutMs}ms`)), timeoutMs);
63
+ promise.then((value) => {
64
+ clearTimeout(timer);
65
+ resolve(value);
66
+ }, (error) => {
67
+ clearTimeout(timer);
68
+ reject(error);
69
+ });
70
+ });
71
+ }
72
+ /**
73
+ * REQ-25 — re-enforces a type's registered `maxItems` clamp at the orchestration layer,
74
+ * independent of the resolver's own discipline (defense in depth, not redundant — see
75
+ * `registry.ts`'s own doc on this point). Applies to any IR node carrying `children` (the shape a
76
+ * list-like resolved widget uses).
77
+ */
78
+ function clampResolveResult(result, registration) {
79
+ if (!result.ok)
80
+ return result;
81
+ const maxItems = registration.clamps.maxItems;
82
+ if (maxItems === undefined || !result.ir.children || result.ir.children.length <= maxItems)
83
+ return result;
84
+ return { ...result, ir: { ...result.ir, children: result.ir.children.slice(0, maxItems) } };
85
+ }
86
+ /**
87
+ * Dispatches to `CORE_RESOLVERS[typeKey].resolveMany` (batch-first, REQ-24), or the static path
88
+ * (REQ-10) for a type with no registered resolver. Owns the sole try/catch + timeout wrapper
89
+ * around every resolver invocation (REQ-26/27) — never throws itself; every failure mode in
90
+ * REQ-27's closed taxonomy converts to a typed `{ ok: false, reason }` result per affected
91
+ * instance.
92
+ *
93
+ * @complexity O(1) dispatch plus whatever the resolver's own `resolveMany` costs (bounded by the
94
+ * registration's `timeoutMs` clamp).
95
+ * @overallScore 100
96
+ */
97
+ /** Sets every instance to the same typed failure — the shared shape behind each "reject the whole batch" branch below. */
98
+ function fillFailure(results, instances, reason) {
99
+ for (const instance of instances)
100
+ results.set(instance.id, { ok: false, reason });
101
+ }
102
+ /** REQ-10: static capability — validated config renders directly, no resolver invocation. */
103
+ function fillStaticResults(results, instances, registration) {
104
+ for (const instance of instances) {
105
+ results.set(instance.id, {
106
+ ok: true,
107
+ ir: { componentId: registration.typeKey, props: instance.config },
108
+ dependencyKeys: [instance.id],
109
+ });
110
+ }
111
+ }
112
+ /** Invokes the resolver under the shared try/catch + timeout boundary (REQ-26/27) and fills `results` per instance. */
113
+ async function fillResolverResults(results, resolver, instances, context, registration) {
114
+ try {
115
+ const resolved = await withResolverTimeout(resolver.resolveMany(instances, context), registration.clamps.timeoutMs);
116
+ for (const instance of instances) {
117
+ const result = resolved.get(instance.id);
118
+ results.set(instance.id, result ? clampResolveResult(result, registration) : { ok: false, reason: "resolver-error" });
119
+ }
120
+ }
121
+ catch (error) {
122
+ const reason = error instanceof ResolverTimeoutError ? "timeout" : "resolver-error";
123
+ fillFailure(results, instances, reason);
124
+ }
125
+ }
126
+ export async function resolveWidgetType(required, _optional = {}) {
127
+ const { typeKey, instances, context } = required;
128
+ const results = new Map();
129
+ if (instances.length === 0)
130
+ return results;
131
+ // `typeKey` is typed `WidgetTypeKey` but only ASSERTED to be one — it's grouped off stored
132
+ // widget-instance data (`resolver-service.ts`'s `payload.widgetType`, itself an
133
+ // `entry-payload.ts` cast over decoded JSON), so `findWidgetTypeRegistration` is the correct,
134
+ // honestly-partial accessor: `"unknown-type"` below is a real, reachable failure mode (REQ-27).
135
+ const registration = findWidgetTypeRegistration({ raw: typeKey });
136
+ if (!registration) {
137
+ fillFailure(results, instances, "unknown-type");
138
+ return results;
139
+ }
140
+ if (!registration.resolverId) {
141
+ fillStaticResults(results, instances, registration);
142
+ return results;
143
+ }
144
+ const resolver = CORE_RESOLVERS[registration.resolverId];
145
+ if (!resolver) {
146
+ fillFailure(results, instances, "resolver-error");
147
+ return results;
148
+ }
149
+ await fillResolverResults(results, resolver, instances, context, registration);
150
+ return results;
151
+ }
152
+ export * from "./ports.js";
153
+ export { createCoreResolvers } from "./create-core-resolvers.js";
154
+ export { resolveTaxonomyEmbeds } from "./taxonomy.js";
155
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,42 @@
1
+ import type { NavMenuReadModel, ResolveTargetHrefFn } from "../../navigation/index.js";
2
+ import type { MenusPort } from "./ports.js";
3
+ import type { WidgetResolver } from "../types.js";
4
+ /**
5
+ * @file `menu` (menu-as-widget) resolver (SPEC-043 REQ-09, ADR-047 §1/§9 — "the primary menu as a
6
+ * widget in the footer region costs nothing new").
7
+ *
8
+ * Purpose:
9
+ * Delegates to `navigation`'s own read model — zero menu STORAGE logic duplicated in `widgets/`
10
+ * (`getMenu`, a real `NavMenuReadModel` call, not a reimplementation of `MenuRepoPort`) — and now
11
+ * also zero menu-HREF-RESOLUTION logic duplicated, via `navigation/resolver.ts`'s exported
12
+ * `resolveMenuDoc` (the doc-level building block `resolveForLocation` itself composes on top of).
13
+ * `resolveItemList`/`resolveItem` stay module-private in `navigation/resolver.ts`; `resolveMenuDoc`
14
+ * is the one exported entry point to their behavior, added specifically so this resolver would not
15
+ * need either an export-widening of those internals or a location-shaped call it doesn't have.
16
+ *
17
+ * Historical limitation (retained to explain the fallback): real href resolution for non-`url` targets (`entryRef`/`termRef`/
18
+ * `route`) needs `src/platform/routing` (ADR-039), which is being built in parallel and is not running code
19
+ * yet anywhere in this codebase — not even `navigation`'s own production callers have a real
20
+ * `ResolveTargetHrefFn` today (`resolveForLocation` takes it as an injected dependency precisely so
21
+ * a fake can stand in until routing lands, per `navigation/resolver.ts`'s own header). This resolver
22
+ * is in the same honest position as the rest of the system, not a special-cased worse one: `url`-kind
23
+ * targets (already-resolved hrefs, no injected dependency needed) now resolve to real, concrete
24
+ * `href`/`available`/`isActive` data; `entryRef`/`termRef`/`route` targets resolve to
25
+ * `available: false, href: null` via the placeholder `resolveTargetHref` below, exactly the outcome
26
+ * `navigation/resolver.ts`'s own doc describes for "cannot resolve" — swap the placeholder for the
27
+ * real routing-backed implementation in one place (`DEFAULT_RESOLVE_TARGET_HREF` below) once
28
+ * `src/platform/routing` ships; no other change needed here.
29
+ * 2026-10-04: runtime roots now inject the routing-backed page/entry resolver. The default remains
30
+ * useful for diagnostics; public widget assembly omits unavailable leaves while retaining parents
31
+ * that still contain visible descendants, matching the static menu's availability contract.
32
+ */
33
+ export interface MenuResolverDeps {
34
+ menus: MenusPort;
35
+ navMenuReadModel: NavMenuReadModel;
36
+ /** Public widgets omit unavailable leaves; diagnostics may retain the complete resolved tree. */
37
+ publicOnly?: boolean;
38
+ /** Host routing injection; callers without it retain the diagnostic placeholder documented above. */
39
+ resolveTargetHref?: ResolveTargetHrefFn;
40
+ }
41
+ export declare function createMenuResolver(deps: MenuResolverDeps, _optional?: Record<string, never>): WidgetResolver;
42
+ //# sourceMappingURL=menu.d.ts.map
@@ -0,0 +1,43 @@
1
+ /** Before host routing was wired every non-URL target was unresolved; retain that safe null fallback. */
2
+ const DEFAULT_RESOLVE_TARGET_HREF = async () => null;
3
+ function toMenuItemProps(items, publicOnly) {
4
+ // Cast: ResolvedNavItem is a plain, JSON-serializable read model (navigation/types.ts) with no
5
+ // index signature of its own — the resolved IR contract only requires structural JSON-compatibility.
6
+ const visible = publicOnly ? items.flatMap((item) => {
7
+ const children = toMenuItemProps(item.children, true);
8
+ return item.available || children.length ? [{ ...item, children }] : [];
9
+ }) : items;
10
+ return visible;
11
+ }
12
+ export function createMenuResolver(deps, _optional = {}) {
13
+ const resolveTargetHref = deps.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
14
+ return {
15
+ async resolveMany(instances, context) {
16
+ const results = new Map();
17
+ for (const instance of instances) {
18
+ const menuRef = typeof instance.config.menuRef === "string" ? instance.config.menuRef : undefined;
19
+ if (!menuRef) {
20
+ results.set(instance.id, { ok: false, reason: "invalid-config" });
21
+ continue;
22
+ }
23
+ const menu = await deps.navMenuReadModel.getMenu({ workspaceId: context.workspaceId, menuId: menuRef });
24
+ if (!menu) {
25
+ results.set(instance.id, { ok: false, reason: "target-disabled" });
26
+ continue;
27
+ }
28
+ const items = await deps.menus.resolveMenuDoc({
29
+ doc: menu.doc,
30
+ context: { workspaceId: context.workspaceId },
31
+ resolveTargetHref,
32
+ });
33
+ results.set(instance.id, {
34
+ ok: true,
35
+ ir: { componentId: "menu", props: { title: menu.title, items: toMenuItemProps(items, deps.publicOnly === true) } },
36
+ dependencyKeys: [menu.id],
37
+ });
38
+ }
39
+ return results;
40
+ },
41
+ };
42
+ }
43
+ //# sourceMappingURL=menu.js.map
@@ -0,0 +1,82 @@
1
+ import type { ContentTypeFieldDef, ContentTypeRecord } from "../../content-types/index.js";
2
+ import type { EntryRecord, EntryStatus } from "../../entries/index.js";
3
+ import type { resolveMenuDoc } from "../../navigation/index.js";
4
+ /** The host's navigation resolver, retained as its owning function's type. */
5
+ export interface MenusPort {
6
+ resolveMenuDoc: typeof resolveMenuDoc;
7
+ }
8
+ export type CollectionListLayout = "cards" | "list" | "accordion" | "carousel";
9
+ export interface CollectionListQuery {
10
+ readonly type: string;
11
+ readonly where: readonly {
12
+ readonly field: string;
13
+ readonly value: string | number | boolean;
14
+ }[];
15
+ readonly sort: {
16
+ readonly by: "published" | "updated" | "title" | {
17
+ readonly field: string;
18
+ };
19
+ readonly dir: "asc" | "desc";
20
+ };
21
+ readonly limit: number;
22
+ }
23
+ export interface EntryDisplayListPort {
24
+ listPublishedForDisplay(required: {
25
+ readonly workspaceId: string;
26
+ readonly query: CollectionListQuery;
27
+ }): Promise<EntryRecord[]>;
28
+ }
29
+ export interface EntryListExcludingTypesPort {
30
+ listByWorkspaceExcludingTypes(required: {
31
+ readonly workspaceId: string;
32
+ readonly excludeTypes: readonly string[];
33
+ readonly status?: EntryStatus;
34
+ readonly orderBy?: "updatedAt";
35
+ readonly orderDirection?: "asc" | "desc";
36
+ readonly limit?: number;
37
+ }): Promise<EntryRecord[]>;
38
+ }
39
+ /** Collection parsing, routing and display policy remain with the host's existing public-list owner. */
40
+ export interface CollectionListPort {
41
+ parseCollectionListConfig(config: unknown, contentType: ContentTypeRecord, options?: {
42
+ readonly defaultSort?: string;
43
+ readonly limitKey?: string;
44
+ }): {
45
+ readonly ok: false;
46
+ readonly reason: string;
47
+ } | {
48
+ readonly ok: true;
49
+ readonly query: CollectionListQuery;
50
+ readonly display: {
51
+ readonly columns: number;
52
+ readonly layout: CollectionListLayout;
53
+ readonly fields: readonly ContentTypeFieldDef[];
54
+ readonly structuredData?: "faq-page";
55
+ };
56
+ };
57
+ entryPublicHref(type: string, slug: string): string | null;
58
+ humanizeFieldName(name: string): string;
59
+ isCollectionListLayout(value: unknown): value is CollectionListLayout;
60
+ readonly systemContentTypes: readonly string[];
61
+ }
62
+ /** Definitions are opaque read snapshots. Submission and notification behavior stays with Forms. */
63
+ export interface FormDefinitionView {
64
+ readonly id: string;
65
+ readonly slug: string;
66
+ readonly status: string;
67
+ readonly fields: readonly unknown[];
68
+ readonly mode?: string;
69
+ readonly html?: string | null;
70
+ }
71
+ /** Both lookup methods are used by today's resolver (id first, then authored slug). */
72
+ export interface FormDefinitionReadPort {
73
+ findById(required: {
74
+ workspaceId: string;
75
+ id: string;
76
+ }): Promise<FormDefinitionView | null>;
77
+ findBySlug(required: {
78
+ workspaceId: string;
79
+ slug: string;
80
+ }): Promise<FormDefinitionView | null>;
81
+ }
82
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,71 @@
1
+ import type { ContentTypeRecord } from "../../content-types/index.js";
2
+ import type { CollectionListPort, EntryDisplayListPort, EntryListExcludingTypesPort } from "./ports.js";
3
+ import type { EntryListPort } from "../../entries/index.js";
4
+ import type { WidgetResolver } from "../types.js";
5
+ /**
6
+ * @file `recent-entries` widget resolver (SPEC-043 REQ-25, ADR-047 §9) — extended by the
7
+ * collections plan (R1, 2026-09-23) into "Collection list": the same widget type now ALSO accepts
8
+ * `collection`/`where`/`sort`/`fields`/`layout`/`columns`, reusing C1's `parseCollectionListConfig`
9
+ * parser and C2's `EntryDisplayListPort.listPublishedForDisplay` query — the exact parser/query the
10
+ * `{"type":"collection"}` marker already uses (`pages.ts`'s `resolveOneCollectionMarker`), not a
11
+ * second implementation of either.
12
+ *
13
+ * Two independent per-instance paths, split by whether `config.collection` names a content type:
14
+ *
15
+ * - **No `collection` (D7, the historical/default shape):** unchanged from before this plan —
16
+ * ONE shared bounded query for the whole batch (`status: 'published'`, `orderBy: 'updatedAt'
17
+ * desc`, `limit: registryMax` — REQ-24/REQ-25, fixed 2026-07-21), across every content type
18
+ * except `SYSTEM_CONTENT_TYPES` (D8) — `widget`/`widget_area`/nav-menu entries were never meant
19
+ * to appear in a "recent entries" feed. That exclusion runs `EntryListExcludingTypesPort.
20
+ * listByWorkspaceExcludingTypes` (review fix 3b, `public-list.ts`) INSIDE the query, not as an
21
+ * in-memory filter on an already limit-bounded `listByWorkspace` result — the earlier
22
+ * filter-after-limit shape could return fewer entries than asked for, or none, whenever
23
+ * `registryMax` system rows were newer than the real content. Nothing else about this path's
24
+ * ordering changed. `layout`/`columns` are still read from the
25
+ * instance's own config (new, additive keys — an old config never set them, so they fall back to
26
+ * `"list"`/`3`, byte-identical to before), but `where`/`sort`/`fields` don't apply — there is no
27
+ * single content type here to validate field names against.
28
+ * - **`collection` set:** `parseCollectionListConfig` validates the config against that content
29
+ * type's declared fields and clamps `limit`/`columns`; `listPublishedForDisplay` runs the bounded,
30
+ * filtered, sorted query. `limitKey: "maxItems"` and `defaultSort: "updated"` are this widget's own
31
+ * legacy aliases (D7) — same parser, different defaults than the marker's `"newest"`/`limit`. The
32
+ * registered clamp is re-applied on top of the parser's own bound (REQ-25 defense in depth: the
33
+ * parser's own ceiling is 24, wider than this registration's 20).
34
+ *
35
+ * Both paths produce the SAME per-item IR shape (`{title, href, dateIso, dateLabel, fields}`,
36
+ * mirroring `entry-list-render.ts`'s `EntryListItem`) under the NEW `"entry-list-item"` componentId
37
+ * — deliberately not the old `"entry-summary"` id (still real, still dispatched, kept for its own
38
+ * `render.test.ts` open-redirect regression — see that file — but no longer emitted by this
39
+ * resolver, so a stray recursive dispatch on these children fails loudly as an unknown component
40
+ * rather than silently mis-rendering the new shape through the old one). `render.ts`'s
41
+ * `renderWidgetRecentEntries` reads these children directly and calls C3's `renderEntryList` for
42
+ * `"cards"` layout.
43
+ *
44
+ * `href` is always `null` today (D1 seam, `entryPublicHref`) — entry pages are off. This is also the
45
+ * fix for the previously-broken `href="/<slug>"` link (`entry-summary`'s own `renderWidgetEntrySummary`
46
+ * built it and it 404s): a `null` href renders the title as plain text (`render.ts`), never a dead
47
+ * link.
48
+ *
49
+ * `categoryTermId` (REQ-32/EC-03) remains a documented soft reference — this resolver does not
50
+ * filter by it (no taxonomy dependency wired here), matching EC-03's explicit "may render as if the
51
+ * filter is empty/unset" allowance.
52
+ */
53
+ /**
54
+ * The narrow read port this resolver needs to look up a `collection` config's target content type —
55
+ * `findByKey` only, not `ContentTypeRepoPort`'s full write/transaction surface (port segregation,
56
+ * same shape-discipline as `EntryDisplayListPort` itself). Both composition roots' real
57
+ * `contentTypeRepo` (`InMemoryContentTypeRepo`/`SqliteContentTypeRepo`) satisfy this structurally.
58
+ */
59
+ export interface ContentTypeLookup {
60
+ findByKey(params: {
61
+ workspaceId: string;
62
+ key: string;
63
+ }): Promise<ContentTypeRecord | null>;
64
+ }
65
+ export interface RecentEntriesResolverDeps {
66
+ collectionList: CollectionListPort;
67
+ entryList: EntryListPort & EntryDisplayListPort & EntryListExcludingTypesPort;
68
+ contentTypes: ContentTypeLookup;
69
+ }
70
+ export declare function createRecentEntriesResolver(deps: RecentEntriesResolverDeps, _optional?: Record<string, never>): WidgetResolver;
71
+ //# sourceMappingURL=recent-entries.d.ts.map