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