@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.
- package/dist/consent/express/index.d.ts +53 -0
- package/dist/consent/express/index.js +159 -0
- package/dist/consent/express/response.d.ts +21 -0
- package/dist/consent/express/response.js +89 -0
- package/dist/consent/html/client.d.ts +6 -0
- package/dist/consent/html/client.js +135 -0
- package/dist/consent/html/gate.d.ts +29 -0
- package/dist/consent/html/gate.js +104 -0
- package/dist/consent/html/index.d.ts +4 -0
- package/dist/consent/html/index.js +3 -0
- package/dist/consent/html/render.d.ts +27 -0
- package/dist/consent/html/render.js +62 -0
- package/dist/consent/html/static.d.ts +7 -0
- package/dist/consent/html/static.js +25 -0
- package/dist/consent/html/styles.d.ts +3 -0
- package/dist/consent/html/styles.js +22 -0
- package/dist/consent/index.d.ts +3 -0
- package/dist/consent/index.js +3 -0
- package/dist/consent/node/index.d.ts +47 -0
- package/dist/consent/node/index.js +169 -0
- package/dist/consent/policy.d.ts +47 -0
- package/dist/consent/policy.js +62 -0
- package/dist/consent/types.d.ts +40 -0
- package/dist/consent/types.js +5 -0
- package/dist/content-types/agent-tools.js +1 -1
- package/dist/content-types/tool-registrations.d.ts +2 -0
- package/dist/content-types/tool-registrations.js +1 -0
- package/dist/content-types/write-service.d.ts +2 -0
- package/dist/content-types/write-service.js +3 -0
- package/dist/forms/descriptor-rules.d.ts +9 -0
- package/dist/forms/descriptor-rules.js +112 -0
- package/dist/forms/email-format.d.ts +7 -0
- package/dist/forms/email-format.js +11 -0
- package/dist/forms/express/handler.js +4 -8
- package/dist/forms/field-registry.d.ts +193 -0
- package/dist/forms/field-registry.js +231 -0
- package/dist/forms/forms.d.ts +8 -5
- package/dist/forms/forms.js +29 -46
- package/dist/forms/html/html-authoring.js +63 -13
- package/dist/forms/html/html-submission-adapter.d.ts +5 -5
- package/dist/forms/html/html-submission-adapter.js +6 -13
- package/dist/forms/html-render.d.ts +10 -3
- package/dist/forms/html-render.js +116 -14
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.js +1 -0
- package/dist/forms/repo.memory.js +4 -4
- package/dist/forms/submission-attempts.d.ts +1 -1
- package/dist/forms/submission-attempts.js +13 -2
- package/dist/forms/submission-email-validation.js +2 -4
- package/dist/forms/submit-service.d.ts +3 -0
- package/dist/forms/submit-service.js +38 -5
- package/dist/forms/types.d.ts +50 -2
- package/dist/listing/batch.d.ts +53 -0
- package/dist/listing/batch.js +57 -0
- package/dist/listing/html.d.ts +72 -0
- package/dist/listing/html.js +109 -0
- package/dist/listing/index.d.ts +5 -0
- package/dist/listing/index.js +5 -0
- package/dist/listing/paths.d.ts +32 -0
- package/dist/listing/paths.js +37 -0
- package/dist/listing/see-more-script.d.ts +16 -0
- package/dist/listing/see-more-script.js +62 -0
- package/dist/media/content-type-sniffer.d.ts +1 -1
- package/dist/media/content-type-sniffer.js +15 -0
- package/dist/media/image-transformer.sharp.js +4 -1
- package/dist/media/media-service.d.ts +4 -0
- package/dist/media/media-service.js +8 -0
- package/dist/media/node.d.ts +1 -1
- package/dist/media/node.js +1 -1
- package/dist/media/transform-registry.js +1 -1
- package/dist/media/transform-types.d.ts +2 -0
- package/dist/media/transform-types.js +3 -0
- package/dist/media/video-frames.ffmpeg.d.ts +15 -2
- package/dist/media/video-frames.ffmpeg.js +28 -13
- package/dist/navigation/agent-tools.js +20 -3
- package/dist/navigation/dropdown.d.ts +41 -0
- package/dist/navigation/dropdown.js +85 -0
- package/dist/navigation/index.d.ts +5 -1
- package/dist/navigation/index.js +4 -0
- package/dist/navigation/menu-html.d.ts +31 -0
- package/dist/navigation/menu-html.js +53 -0
- package/dist/navigation/menu-service.d.ts +9 -3
- package/dist/navigation/menu-service.js +15 -3
- package/dist/navigation/tool-registrations.d.ts +5 -0
- package/dist/navigation/tool-registrations.js +32 -2
- package/dist/navigation/types.d.ts +6 -0
- package/dist/redirects/capture.d.ts +2 -0
- package/dist/redirects/capture.js +4 -0
- package/dist/redirects/redirects.d.ts +8 -0
- package/dist/redirects/redirects.js +14 -0
- package/dist/routing/index.d.ts +5 -0
- package/dist/routing/index.js +2 -0
- package/dist/routing/ports.d.ts +51 -0
- package/dist/routing/routing.d.ts +138 -0
- package/dist/routing/routing.js +443 -0
- package/dist/routing/types.d.ts +164 -0
- package/dist/routing/types.js +2 -0
- package/dist/seo/media.d.ts +23 -0
- package/dist/seo/media.js +30 -5
- package/dist/seo/page-head.js +8 -1
- package/dist/seo/seo.d.ts +2 -0
- package/dist/seo/seo.js +11 -6
- package/dist/seo/types.d.ts +8 -0
- package/dist/seo/xml-escape.d.ts +1 -3
- package/dist/seo/xml-escape.js +1 -6
- package/dist/widgets/concurrency.d.ts +27 -1
- package/dist/widgets/concurrency.js +12 -23
- package/dist/widgets/config-validation.js +6 -0
- package/dist/widgets/gallery-resolver.d.ts +23 -0
- package/dist/widgets/gallery-resolver.js +80 -0
- package/dist/widgets/html/gallery-render.d.ts +24 -0
- package/dist/widgets/html/gallery-render.js +65 -0
- package/dist/widgets/html/html-embeds.d.ts +4 -0
- package/dist/widgets/html/html-embeds.js +7 -1
- package/dist/widgets/html/index.d.ts +3 -0
- package/dist/widgets/html/index.js +3 -0
- package/dist/widgets/html/server-fragment.d.ts +9 -0
- package/dist/widgets/html/server-fragment.js +38 -0
- package/dist/widgets/markers/gallery.d.ts +50 -0
- package/dist/widgets/markers/gallery.js +164 -0
- package/dist/widgets/markers/html-attributes.d.ts +6 -1
- package/dist/widgets/markers/html-attributes.js +24 -9
- package/dist/widgets/markers/index.d.ts +2 -0
- package/dist/widgets/markers/index.js +2 -0
- package/dist/widgets/markers/marker.d.ts +4 -0
- package/dist/widgets/markers/marker.js +6 -0
- package/dist/widgets/markers/media-reference.d.ts +11 -0
- package/dist/widgets/markers/media-reference.js +11 -0
- package/dist/widgets/page-embed-types.d.ts +1 -1
- package/dist/widgets/page-embed-types.js +1 -1
- package/dist/widgets/registry.d.ts +144 -10
- package/dist/widgets/registry.js +40 -0
- package/dist/widgets/resolver-service.d.ts +10 -0
- package/dist/widgets/resolver-service.js +73 -29
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +3 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +2 -0
- package/dist/widgets/resolvers/gallery.d.ts +12 -0
- package/dist/widgets/resolvers/gallery.js +33 -0
- package/dist/widgets/resolvers/index.d.ts +1 -0
- package/dist/widgets/resolvers/index.js +1 -0
- package/dist/widgets/resolvers/menu.d.ts +8 -0
- package/dist/widgets/resolvers/menu.js +21 -6
- package/dist/widgets/types.d.ts +3 -1
- package/package.json +51 -6
- package/dist/http/settings/cms-adapter.d.ts +0 -20
- package/dist/http/settings/cms-adapter.js +0 -124
- package/dist/http/settings/contracts.d.ts +0 -135
- package/dist/http/settings/contracts.js +0 -2
- package/dist/http/settings/http.d.ts +0 -10
- package/dist/http/settings/http.js +0 -211
- package/dist/http/settings/index.d.ts +0 -5
- package/dist/http/settings/index.js +0 -4
- package/dist/http/settings/shared.d.ts +0 -68
- package/dist/http/settings/shared.js +0 -54
- package/dist/http/settings/stream.d.ts +0 -24
- package/dist/http/settings/stream.js +0 -147
- package/dist/settings/agent-tools.d.ts +0 -10
- package/dist/settings/agent-tools.js +0 -204
- package/dist/settings/agent-value-write-tools.d.ts +0 -22
- package/dist/settings/agent-value-write-tools.js +0 -154
- package/dist/settings/agent-writable-preferences.d.ts +0 -67
- package/dist/settings/agent-writable-preferences.js +0 -132
- package/dist/settings/agent-write-denylist.d.ts +0 -19
- package/dist/settings/agent-write-denylist.js +0 -15
- package/dist/settings/change-feed.d.ts +0 -83
- package/dist/settings/change-feed.js +0 -61
- package/dist/settings/definitions-dispatch.d.ts +0 -56
- package/dist/settings/definitions-dispatch.js +0 -86
- package/dist/settings/dictionaries/index.d.ts +0 -48
- package/dist/settings/dictionaries/index.js +0 -69
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -27
- package/dist/settings/ensure-definitions.d.ts +0 -88
- package/dist/settings/ensure-definitions.js +0 -91
- package/dist/settings/errors.d.ts +0 -42
- package/dist/settings/errors.js +0 -46
- package/dist/settings/express/cms-adapter.d.ts +0 -20
- package/dist/settings/express/cms-adapter.js +0 -124
- package/dist/settings/express/contracts.d.ts +0 -135
- package/dist/settings/express/contracts.js +0 -2
- package/dist/settings/express/http.d.ts +0 -10
- package/dist/settings/express/http.js +0 -211
- package/dist/settings/express/shared.d.ts +0 -68
- package/dist/settings/express/shared.js +0 -54
- package/dist/settings/express/stream.d.ts +0 -24
- package/dist/settings/express/stream.js +0 -147
- package/dist/settings/ports.d.ts +0 -118
- package/dist/settings/principal-lookup.d.ts +0 -9
- package/dist/settings/principal-lookup.js +0 -2
- package/dist/settings/purge-service.d.ts +0 -54
- package/dist/settings/purge-service.js +0 -106
- package/dist/settings/repo.memory.d.ts +0 -119
- package/dist/settings/repo.memory.js +0 -199
- package/dist/settings/settings.d.ts +0 -139
- package/dist/settings/settings.js +0 -259
- package/dist/settings/tool-registrations.d.ts +0 -51
- package/dist/settings/tool-registrations.js +0 -250
- package/dist/settings/types.d.ts +0 -125
- package/dist/settings/types.js +0 -3
- package/dist/settings/ui-tab-definitions.d.ts +0 -41
- package/dist/settings/ui-tab-definitions.js +0 -148
- package/dist/settings/write-service.d.ts +0 -320
- package/dist/settings/write-service.js +0 -683
- /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
|
package/dist/seo/media.d.ts
CHANGED
|
@@ -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
|