@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
|
@@ -151,7 +151,10 @@ export class SharpImageTransformer {
|
|
|
151
151
|
fit: input.params.fit ?? "cover",
|
|
152
152
|
});
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
// JPEG cannot carry alpha; explicit white avoids libvips' black background for logos.
|
|
155
|
+
if (input.params.format === "jpeg")
|
|
156
|
+
pipeline = pipeline.flatten({ background: "#ffffff" });
|
|
157
|
+
pipeline = pipeline.toFormat(input.params.format, input.params.quality === undefined ? undefined : { quality: input.params.quality });
|
|
155
158
|
const outBuffer = await pipeline.toBuffer();
|
|
156
159
|
return {
|
|
157
160
|
bytes: new Uint8Array(outBuffer),
|
|
@@ -27,6 +27,10 @@ export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
|
27
27
|
* `routes/site/media-rendition.ts`). This widens the SAME advisory,
|
|
28
28
|
* client-declared-string check item 4 above already discloses as untrusted —
|
|
29
29
|
* no new ingress hardening was added for video.
|
|
30
|
+
*
|
|
31
|
+
* Audio (owner-approved mediawidget, 2026-10-10/11) follows that same original-byte
|
|
32
|
+
* lifecycle. Only formats recognized by the shared sniffer are allowed; no image
|
|
33
|
+
* transform is requested for them.
|
|
30
34
|
*/
|
|
31
35
|
export declare const DEFAULT_ALLOWED_MIME_TYPES: ReadonlySet<string>;
|
|
32
36
|
/**
|
|
@@ -100,6 +100,10 @@ function formatMaxUploadBytesAsMb(bytes) {
|
|
|
100
100
|
* `routes/site/media-rendition.ts`). This widens the SAME advisory,
|
|
101
101
|
* client-declared-string check item 4 above already discloses as untrusted —
|
|
102
102
|
* no new ingress hardening was added for video.
|
|
103
|
+
*
|
|
104
|
+
* Audio (owner-approved mediawidget, 2026-10-10/11) follows that same original-byte
|
|
105
|
+
* lifecycle. Only formats recognized by the shared sniffer are allowed; no image
|
|
106
|
+
* transform is requested for them.
|
|
103
107
|
*/
|
|
104
108
|
export const DEFAULT_ALLOWED_MIME_TYPES = new Set([
|
|
105
109
|
"image/jpeg",
|
|
@@ -109,6 +113,10 @@ export const DEFAULT_ALLOWED_MIME_TYPES = new Set([
|
|
|
109
113
|
"image/avif",
|
|
110
114
|
"video/mp4",
|
|
111
115
|
"video/webm",
|
|
116
|
+
"audio/wav",
|
|
117
|
+
"audio/mpeg",
|
|
118
|
+
"audio/flac",
|
|
119
|
+
"audio/mp4",
|
|
112
120
|
]);
|
|
113
121
|
/**
|
|
114
122
|
* Enforces the "settable-once-from-absent" write-once rule for
|
package/dist/media/node.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Node-bound media adapters; hosts supply the filesystem and optional codecs. */
|
|
2
2
|
export { LocalFsBlobStore, type LocalFsBlobStoreDeps } from "./blob-store.fs.js";
|
|
3
3
|
export { SharpImageTransformer, ImageTransformUnavailableError, ImageSourceCorruptError } from "./image-transformer.sharp.js";
|
|
4
|
-
export { createFfmpegVideoFrameExtractor, findVideoBinaries, runLowPriorityVideoProcess, type VideoBinaries, type VideoBinaryFinder, type VideoProcessRunner, } from './video-frames.ffmpeg.js';
|
|
4
|
+
export { createFfmpegVideoFrameExtractor, createVideoExtractionLock, findVideoBinaries, runLowPriorityVideoProcess, type VideoExtractionLock, type VideoBinaries, type VideoBinaryFinder, type VideoProcessRunner, } from './video-frames.ffmpeg.js';
|
|
5
5
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/media/node.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
export { LocalFsBlobStore } from "./blob-store.fs.js";
|
|
3
3
|
export { SharpImageTransformer, ImageTransformUnavailableError, ImageSourceCorruptError } from "./image-transformer.sharp.js";
|
|
4
4
|
// Video previews: policy/port plus a bounded optional-host codec adapter.
|
|
5
|
-
export { createFfmpegVideoFrameExtractor, findVideoBinaries, runLowPriorityVideoProcess, } from './video-frames.ffmpeg.js';
|
|
5
|
+
export { createFfmpegVideoFrameExtractor, createVideoExtractionLock, findVideoBinaries, runLowPriorityVideoProcess, } from './video-frames.ffmpeg.js';
|
|
6
6
|
//# sourceMappingURL=node.js.map
|
|
@@ -26,7 +26,7 @@ export async function registerTransform(required, _optional = {}) {
|
|
|
26
26
|
const owner = input.owner.trim();
|
|
27
27
|
if (!owner)
|
|
28
28
|
throw new TransformValidationError({ message: "owner is required" });
|
|
29
|
-
assertValidTransformParams({ format: input.params.format }, { width: input.params.width, height: input.params.height, fit: input.params.fit });
|
|
29
|
+
assertValidTransformParams({ format: input.params.format }, { width: input.params.width, height: input.params.height, fit: input.params.fit, quality: input.params.quality });
|
|
30
30
|
return withTransformRegistryLock({ key: `${input.workspaceId}:${name}`, criticalSection: async () => {
|
|
31
31
|
const existingVersions = await deps.transformRepo.listByName({ workspaceId: input.workspaceId, name });
|
|
32
32
|
const currentMaxVersion = existingVersions.reduce((max, row) => Math.max(max, row.version), 0);
|
|
@@ -41,6 +41,8 @@ export interface TransformParams {
|
|
|
41
41
|
height?: number | undefined;
|
|
42
42
|
fit?: TransformFit | undefined;
|
|
43
43
|
format: TransformFormat;
|
|
44
|
+
/** Encoder quality, 1–100; omitted preserves the codec default. */
|
|
45
|
+
quality?: number | undefined;
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* `transform_registry` sidecar row. Identity is
|
|
@@ -45,6 +45,9 @@ export function assertValidTransformParams(required, optional = {}) {
|
|
|
45
45
|
throw new TransformValidationError({ message: `transform params.${key} must be an integer between 1 and ${MAX_TRANSFORM_DIMENSION_PX}` });
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
if (params.quality !== undefined && (!Number.isInteger(params.quality) || params.quality < 1 || params.quality > 100)) {
|
|
49
|
+
throw new TransformValidationError({ message: "transform params.quality must be an integer between 1 and 100" });
|
|
50
|
+
}
|
|
48
51
|
if (params.fit !== undefined && !(params.width && params.height)) {
|
|
49
52
|
throw new TransformValidationError({ message: "transform params.fit requires both width and height" });
|
|
50
53
|
}
|
|
@@ -5,7 +5,10 @@ export interface VideoBinaries {
|
|
|
5
5
|
readonly ffmpeg: string;
|
|
6
6
|
readonly ffprobe: string;
|
|
7
7
|
}
|
|
8
|
-
export type VideoBinaryFinder = (required: Record<string, never>, optional:
|
|
8
|
+
export type VideoBinaryFinder = (required: Record<string, never>, optional: {
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
platform?: NodeJS.Platform;
|
|
11
|
+
}) => Promise<VideoBinaries | undefined>;
|
|
9
12
|
export type VideoProcessRunner = (required: {
|
|
10
13
|
command: string;
|
|
11
14
|
args: readonly string[];
|
|
@@ -31,7 +34,17 @@ export declare const runLowPriorityVideoProcess: ({ command, args, spawnProcess,
|
|
|
31
34
|
maxOutputBytes: number;
|
|
32
35
|
signal?: AbortSignal;
|
|
33
36
|
}) => Promise<Uint8Array>;
|
|
34
|
-
export
|
|
37
|
+
export interface VideoExtractionLock {
|
|
38
|
+
/** Synchronous acquisition prevents races before the first awaited probe. */
|
|
39
|
+
tryAcquire(required: Record<string, never>, optional: Record<string, never>): (() => void) | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** A host may share this lock across its adapters to enforce a decoder budget.
|
|
42
|
+
* Returns a synchronous try-acquire port; release follows decoder cleanup. @complexity O(1). */
|
|
43
|
+
export declare function createVideoExtractionLock(_required: Record<string, never>, _optional?: Record<string, never>): VideoExtractionLock;
|
|
44
|
+
export declare function createFfmpegVideoFrameExtractor({ findBinaries, runProcess, nowMs, lock, env, platform }: {
|
|
45
|
+
lock?: VideoExtractionLock;
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
47
|
+
platform?: NodeJS.Platform;
|
|
35
48
|
findBinaries?: VideoBinaryFinder;
|
|
36
49
|
runProcess?: VideoProcessRunner;
|
|
37
50
|
nowMs?: (required: Record<string, never>, optional: Record<string, never>) => number;
|
|
@@ -13,9 +13,9 @@ class VideoProcessError extends Error {
|
|
|
13
13
|
this.reason = reason;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
async function executable(required, { configured } = {}) {
|
|
16
|
+
async function executable(required, { configured, env = process.env } = {}) {
|
|
17
17
|
const candidates = configured ? [configured] : [
|
|
18
|
-
...(
|
|
18
|
+
...(env['PATH'] ?? '').split(delimiter).filter(isAbsolute).map(directory => join(directory, required.name)),
|
|
19
19
|
join('/opt/homebrew/bin', required.name), join('/usr/local/bin', required.name), join('/usr/bin', required.name),
|
|
20
20
|
];
|
|
21
21
|
for (const candidate of candidates) {
|
|
@@ -32,12 +32,12 @@ async function executable(required, { configured } = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
/** Discovery never executes a version/probe command. Configured paths are host configuration,
|
|
34
34
|
* never tool arguments, and all subprocesses (including ffprobe) still go through nice. */
|
|
35
|
-
export const findVideoBinaries = async (_required,
|
|
36
|
-
if (
|
|
35
|
+
export const findVideoBinaries = async (_required, { env = process.env, platform = process.platform } = {}) => {
|
|
36
|
+
if (platform === 'win32')
|
|
37
37
|
return undefined; // Never silently omit the low-priority guarantee.
|
|
38
|
-
const nice = await executable({ name: 'nice' }, {});
|
|
39
|
-
const ffmpeg = await executable({ name: 'ffmpeg' }, { configured:
|
|
40
|
-
const ffprobe = await executable({ name: 'ffprobe' }, { configured:
|
|
38
|
+
const nice = await executable({ name: 'nice' }, { env });
|
|
39
|
+
const ffmpeg = await executable({ name: 'ffmpeg' }, { configured: env['FFMPEG_PATH'], env });
|
|
40
|
+
const ffprobe = await executable({ name: 'ffprobe' }, { configured: env['FFPROBE_PATH'], env });
|
|
41
41
|
return nice && ffmpeg && ffprobe ? { nice, ffmpeg, ffprobe } : undefined;
|
|
42
42
|
};
|
|
43
43
|
/** detached creates a group including nice and its eventual executable; killing the group prevents
|
|
@@ -99,25 +99,40 @@ export const runLowPriorityVideoProcess = async ({ command, args, spawnProcess =
|
|
|
99
99
|
abort();
|
|
100
100
|
});
|
|
101
101
|
};
|
|
102
|
+
/** A host may share this lock across its adapters to enforce a decoder budget.
|
|
103
|
+
* Returns a synchronous try-acquire port; release follows decoder cleanup. @complexity O(1). */
|
|
104
|
+
export function createVideoExtractionLock(_required, _optional = {}) {
|
|
105
|
+
let extracting = false;
|
|
106
|
+
return { tryAcquire() {
|
|
107
|
+
if (extracting)
|
|
108
|
+
return undefined;
|
|
109
|
+
extracting = true;
|
|
110
|
+
let released = false;
|
|
111
|
+
return () => { if (!released) {
|
|
112
|
+
released = true;
|
|
113
|
+
extracting = false;
|
|
114
|
+
} };
|
|
115
|
+
} };
|
|
116
|
+
}
|
|
102
117
|
// Shared across ALL adapter instances: a second catalog/tenant cannot run a second ffmpeg in this
|
|
103
118
|
// process. Fail busy instead of holding arbitrary numbers of uploaded videos in a pending queue.
|
|
104
|
-
|
|
119
|
+
const defaultExtractionLock = createVideoExtractionLock({});
|
|
105
120
|
const UNAVAILABLE = 'video preview unavailable on this host (nice, ffmpeg and ffprobe are required).';
|
|
106
121
|
// Eight amortizes process/seek overhead while bounding expensive source decoders to
|
|
107
122
|
// one eighth of the 64-frame maximum. Thread caps alone do not bound decoder memory.
|
|
108
123
|
// This is a decoder-count limit, not a measured host RSS guarantee.
|
|
109
124
|
const SOURCE_BATCH_SIZE = 8;
|
|
110
125
|
const localInputFlags = ['-threads', '1', '-protocol_whitelist', 'file', '-format_whitelist', 'mov,matroska,webm'];
|
|
111
|
-
export function createFfmpegVideoFrameExtractor({ findBinaries = findVideoBinaries, runProcess = runLowPriorityVideoProcess, nowMs = () => performance.now() }, _optional = {}) {
|
|
126
|
+
export function createFfmpegVideoFrameExtractor({ findBinaries = findVideoBinaries, runProcess = runLowPriorityVideoProcess, nowMs = () => performance.now(), lock = defaultExtractionLock, env, platform }, _optional = {}) {
|
|
112
127
|
return { extract: async ({ bytes }, options) => {
|
|
113
128
|
if (bytes.byteLength > VIDEO_MAX_INPUT_BYTES)
|
|
114
129
|
return { ok: false, reason: 'too-large', message: `Video preview input exceeds ${VIDEO_MAX_INPUT_BYTES} bytes.` };
|
|
115
130
|
const type = sniffContentType({ bytes });
|
|
116
131
|
if (type !== 'video/mp4' && type !== 'video/webm')
|
|
117
132
|
return { ok: false, reason: 'invalid', message: 'Video preview requires stored MP4 or WebM bytes.' };
|
|
118
|
-
|
|
133
|
+
const release = lock.tryAcquire({}, {});
|
|
134
|
+
if (!release)
|
|
119
135
|
return { ok: false, reason: 'busy', message: 'Video preview is busy. Try again after the current extraction finishes.' };
|
|
120
|
-
extracting = true;
|
|
121
136
|
const deadline = nowMs({}, {}) + VIDEO_EXTRACTION_TIMEOUT_MS;
|
|
122
137
|
let directory;
|
|
123
138
|
try {
|
|
@@ -125,7 +140,7 @@ export function createFfmpegVideoFrameExtractor({ findBinaries = findVideoBinari
|
|
|
125
140
|
throw new VideoProcessError('canceled', 'Video preview canceled.');
|
|
126
141
|
if (options.layout !== undefined && options.layout !== 'frames' && options.layout !== 'sheet')
|
|
127
142
|
throw new VideoProcessError('invalid', 'Video layout must be frames or sheet.');
|
|
128
|
-
const binaries = await findBinaries({}, {});
|
|
143
|
+
const binaries = await findBinaries({}, { ...(env ? { env } : {}), ...(platform ? { platform } : {}) });
|
|
129
144
|
if (!binaries)
|
|
130
145
|
return { ok: false, reason: 'unavailable', message: UNAVAILABLE };
|
|
131
146
|
directory = await mkdtemp(join(tmpdir(), 'video-preview-'));
|
|
@@ -253,7 +268,7 @@ export function createFfmpegVideoFrameExtractor({ findBinaries = findVideoBinari
|
|
|
253
268
|
return { ok: false, reason: 'invalid', message: 'Video preview temporary files could not be removed.' };
|
|
254
269
|
}
|
|
255
270
|
finally {
|
|
256
|
-
|
|
271
|
+
release();
|
|
257
272
|
}
|
|
258
273
|
}
|
|
259
274
|
} };
|
|
@@ -71,6 +71,18 @@ const MENU_ID_SCHEMA = {
|
|
|
71
71
|
type: "string",
|
|
72
72
|
description: "The menu's id, as returned by menus_list_menus, menus_get_menu, or menus_create_menu.",
|
|
73
73
|
};
|
|
74
|
+
/** HTML mode (2026-10-08, `menu-html.ts`): the same two fields on create and update. */
|
|
75
|
+
const MENU_MODE_SCHEMA = {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["items", "html"],
|
|
78
|
+
description: "What the site renders for this menu: 'items' (the item tree, the default) or 'html' (the menu's own 'html'). Switching keeps the other mode's data. Omit to leave unchanged.",
|
|
79
|
+
};
|
|
80
|
+
const MENU_HTML_SCHEMA = {
|
|
81
|
+
type: "string",
|
|
82
|
+
maxLength: 200000,
|
|
83
|
+
description: "The menu's own HTML for 'html' mode — any markup and classes, e.g. '<ul class=\"nav\"><li><a href=\"/\">Home</a></li></ul>'. " +
|
|
84
|
+
"Rendered inside the theme element that holds the menu's marker. Needs raw-HTML permission (admins/owners). Omit to leave unchanged.",
|
|
85
|
+
};
|
|
74
86
|
/**
|
|
75
87
|
* A link target, published as a `oneOf` over the 4 v1 kinds `menu-service.ts`'s `validateTarget`
|
|
76
88
|
* accepts (`entryRef`/`termRef`/`url`/`route`) — `dynamicQuery`/`content` are named-but-rejected
|
|
@@ -208,6 +220,8 @@ export const menusAgentToolCatalog = [
|
|
|
208
220
|
title: { type: "string", minLength: 1, description: "Human-readable menu name." },
|
|
209
221
|
slug: { type: "string", pattern: "^[a-z0-9-]+$", description: "Stable machine handle. Lowercase letters, digits, and hyphens only." },
|
|
210
222
|
items: { ...ITEMS_TREE_SCHEMA, description: "Optional initial item tree. Defaults to an empty menu if omitted." },
|
|
223
|
+
mode: MENU_MODE_SCHEMA,
|
|
224
|
+
html: MENU_HTML_SCHEMA,
|
|
211
225
|
},
|
|
212
226
|
},
|
|
213
227
|
},
|
|
@@ -215,19 +229,22 @@ export const menusAgentToolCatalog = [
|
|
|
215
229
|
name: "menus_update_menu_tree",
|
|
216
230
|
description: "Replaces an existing menu's whole item tree (and optionally its title/slug) in one version-guarded write. " +
|
|
217
231
|
"Always a whole-tree replace — there is no per-item patch operation, so the submitted tree must include every " +
|
|
218
|
-
"item the menu keeps, each with its fields copied unchanged from the read.
|
|
232
|
+
"item the menu keeps, each with its fields copied unchanged from the read. For a hand-written HTML menu, send " +
|
|
233
|
+
"'html' (and 'mode': 'html') and omit 'items' to keep the stored tree. To delete a menu, use trash_item with entityType 'menu' (it moves to the Trash and can be restored).",
|
|
219
234
|
sideEffects: "mutates-durable-state",
|
|
220
235
|
authorization: { permission: "admin.menus.update" },
|
|
221
236
|
inputSchema: {
|
|
222
237
|
type: "object",
|
|
223
238
|
additionalProperties: false,
|
|
224
|
-
required: ["menuId", "expectedVersion"
|
|
239
|
+
required: ["menuId", "expectedVersion"],
|
|
225
240
|
properties: {
|
|
226
241
|
menuId: MENU_ID_SCHEMA,
|
|
227
242
|
expectedVersion: { type: "integer", description: "The menu's current 'version', as last returned by a read or write on it — the optimistic-concurrency guard. A stale value is rejected with a conflict naming the current version." },
|
|
228
243
|
title: { type: "string", description: "New title. Omit to leave unchanged." },
|
|
229
244
|
slug: { type: "string", pattern: "^[a-z0-9-]+$", description: "New slug. Omit to leave unchanged. Must not collide with another menu's slug." },
|
|
230
|
-
items: ITEMS_TREE_SCHEMA,
|
|
245
|
+
items: { ...ITEMS_TREE_SCHEMA, description: "The full replacement item tree. Omit to keep the stored tree (e.g. an HTML-only edit); send at least one of items, html or mode." },
|
|
246
|
+
mode: MENU_MODE_SCHEMA,
|
|
247
|
+
html: MENU_HTML_SCHEMA,
|
|
231
248
|
},
|
|
232
249
|
},
|
|
233
250
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dropdown navigation (`"variant":"dropdown"` menus), following the APG disclosure-navigation
|
|
3
|
+
* pattern: a branch item carries a server-rendered `<button class="jini-menu-toggle" aria-expanded
|
|
4
|
+
* aria-controls>` pointing at its child `<ul id>`, and the root `<ul>` carries `data-jini-dropdown`.
|
|
5
|
+
* No `role="menu"` and no `aria-haspopup` — those make screen readers announce a menu and expect
|
|
6
|
+
* menubar keys, which site nav should not imitate.
|
|
7
|
+
*
|
|
8
|
+
* The toggles are real buttons in the markup (not injected by JS), so without JS Tab still reaches
|
|
9
|
+
* them and a theme's `:focus-within` CSS opens the panel. {@link MENU_DROPDOWN_SCRIPT} adds
|
|
10
|
+
* `jini-menu-js` to each root so a theme can switch that fallback off once JS owns the open state
|
|
11
|
+
* (otherwise Escape could not close a panel while focus sits on its toggle).
|
|
12
|
+
*/
|
|
13
|
+
/** A branch item's toggle. Split form (default): an empty button labelled "`label` submenu" that sits
|
|
14
|
+
* after the item's link. `asLabel`: the button IS the item's label (a link-less parent), and carries
|
|
15
|
+
* `menu-item-label` so theme scripts that read `.menu-item-label` treat it as an inert parent.
|
|
16
|
+
* @complexity O(n) over `label`. */
|
|
17
|
+
export declare function renderMenuToggle({ controlsId, label }: {
|
|
18
|
+
controlsId: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}, { asLabel }?: {
|
|
21
|
+
asLabel?: boolean;
|
|
22
|
+
}): string;
|
|
23
|
+
/**
|
|
24
|
+
* The inline script behind every `[data-jini-dropdown]` menu ({@link injectMenuDropdownScript} adds it
|
|
25
|
+
* once per page). State lives in `li.jini-menu-open` and the toggle's `aria-expanded`.
|
|
26
|
+
* - click on a toggle opens/closes its item (mouse, touch tap, Enter/Space); any click or focus
|
|
27
|
+
* elsewhere closes every open item that does not contain the target (siblings, click-outside,
|
|
28
|
+
* tabbing out). Closing an item closes its open descendants too.
|
|
29
|
+
* - Escape closes the nearest open item around the focus and returns focus to its toggle.
|
|
30
|
+
* - ArrowDown on a toggle opens it and focuses the submenu's first link/button; ArrowDown/ArrowUp
|
|
31
|
+
* inside a submenu move between sibling items; ArrowLeft/ArrowRight on a top-level item move between
|
|
32
|
+
* top-level items. No wrap at either end. Handled arrows `preventDefault` so the page does not scroll.
|
|
33
|
+
* Hover stays pure theme CSS and never touches `aria-expanded`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MENU_DROPDOWN_SCRIPT = "(function(){\n var roots=document.querySelectorAll('[data-jini-dropdown]');\n for(var i=0;i<roots.length;i++)roots[i].classList.add('jini-menu-js');\n function inMenu(el){return el&&el.closest&&el.closest('[data-jini-dropdown]')?el:null;}\n function toggleOf(li){return li.querySelector(':scope > .jini-menu-toggle');}\n function panelOf(li){var b=toggleOf(li);return b?document.getElementById(b.getAttribute('aria-controls')):null;}\n function first(li){return li?li.querySelector('a[href], button'):null;}\n function setOpen(li,open){\n var items=open?[li]:[li].concat(Array.prototype.slice.call(li.querySelectorAll('li.jini-menu-open')));\n for(var i=0;i<items.length;i++){items[i].classList[open?'add':'remove']('jini-menu-open');var b=toggleOf(items[i]);if(b)b.setAttribute('aria-expanded',open?'true':'false');}\n }\n function closeAll(keep){\n var open=document.querySelectorAll('[data-jini-dropdown] li.jini-menu-open');\n for(var i=0;i<open.length;i++)if(!(keep&&keep.nodeType===1&&open[i].contains(keep)))setOpen(open[i],false);\n }\n document.addEventListener('focusin',function(e){closeAll(e.target);});\n document.addEventListener('click',function(e){\n closeAll(e.target);\n var b=inMenu(e.target)?e.target.closest('.jini-menu-toggle'):null;\n if(b)setOpen(b.parentNode,b.getAttribute('aria-expanded')!=='true');\n });\n document.addEventListener('keydown',function(e){\n var t=inMenu(e.target);if(!t)return;\n if(e.key==='Escape'){var o=t.closest('li.jini-menu-open');if(o){setOpen(o,false);toggleOf(o).focus();}return;}\n var li=t.closest('li'),next=null;if(!li)return;\n var top=li.parentNode.hasAttribute('data-jini-dropdown');\n if(e.key==='ArrowDown'&&t.classList.contains('jini-menu-toggle')){closeAll(li);setOpen(li,true);var p=panelOf(li);next=p?first(p):null;}\n else if(!top&&e.key==='ArrowDown')next=first(li.nextElementSibling);\n else if(!top&&e.key==='ArrowUp')next=first(li.previousElementSibling);\n else if(top&&e.key==='ArrowRight')next=first(li.nextElementSibling);\n else if(top&&e.key==='ArrowLeft')next=first(li.previousElementSibling);\n else return;\n e.preventDefault();\n if(next)next.focus();\n });\n })();";
|
|
36
|
+
/** Adds the one inline dropdown script before `</body>` when the page carries a `data-jini-dropdown`
|
|
37
|
+
* menu and does not already have the script. @complexity O(n). */
|
|
38
|
+
export declare function injectMenuDropdownScript({ html }: {
|
|
39
|
+
html: string;
|
|
40
|
+
}, _optional?: Record<string, never>): string;
|
|
41
|
+
//# sourceMappingURL=dropdown.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dropdown navigation (`"variant":"dropdown"` menus), following the APG disclosure-navigation
|
|
3
|
+
* pattern: a branch item carries a server-rendered `<button class="jini-menu-toggle" aria-expanded
|
|
4
|
+
* aria-controls>` pointing at its child `<ul id>`, and the root `<ul>` carries `data-jini-dropdown`.
|
|
5
|
+
* No `role="menu"` and no `aria-haspopup` — those make screen readers announce a menu and expect
|
|
6
|
+
* menubar keys, which site nav should not imitate.
|
|
7
|
+
*
|
|
8
|
+
* The toggles are real buttons in the markup (not injected by JS), so without JS Tab still reaches
|
|
9
|
+
* them and a theme's `:focus-within` CSS opens the panel. {@link MENU_DROPDOWN_SCRIPT} adds
|
|
10
|
+
* `jini-menu-js` to each root so a theme can switch that fallback off once JS owns the open state
|
|
11
|
+
* (otherwise Escape could not close a panel while focus sits on its toggle).
|
|
12
|
+
*/
|
|
13
|
+
const SCRIPT_MARKER = 'data-jini-menu-dropdown-script';
|
|
14
|
+
function escapeAttribute(value) {
|
|
15
|
+
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
16
|
+
}
|
|
17
|
+
/** A branch item's toggle. Split form (default): an empty button labelled "`label` submenu" that sits
|
|
18
|
+
* after the item's link. `asLabel`: the button IS the item's label (a link-less parent), and carries
|
|
19
|
+
* `menu-item-label` so theme scripts that read `.menu-item-label` treat it as an inert parent.
|
|
20
|
+
* @complexity O(n) over `label`. */
|
|
21
|
+
export function renderMenuToggle({ controlsId, label }, { asLabel = false } = {}) {
|
|
22
|
+
const id = escapeAttribute(controlsId);
|
|
23
|
+
const text = escapeAttribute(label);
|
|
24
|
+
return asLabel
|
|
25
|
+
? `<button type="button" class="menu-item-label jini-menu-toggle" aria-expanded="false" aria-controls="${id}">${text}</button>`
|
|
26
|
+
: `<button type="button" class="jini-menu-toggle" aria-expanded="false" aria-controls="${id}" aria-label="${text} submenu"></button>`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The inline script behind every `[data-jini-dropdown]` menu ({@link injectMenuDropdownScript} adds it
|
|
30
|
+
* once per page). State lives in `li.jini-menu-open` and the toggle's `aria-expanded`.
|
|
31
|
+
* - click on a toggle opens/closes its item (mouse, touch tap, Enter/Space); any click or focus
|
|
32
|
+
* elsewhere closes every open item that does not contain the target (siblings, click-outside,
|
|
33
|
+
* tabbing out). Closing an item closes its open descendants too.
|
|
34
|
+
* - Escape closes the nearest open item around the focus and returns focus to its toggle.
|
|
35
|
+
* - ArrowDown on a toggle opens it and focuses the submenu's first link/button; ArrowDown/ArrowUp
|
|
36
|
+
* inside a submenu move between sibling items; ArrowLeft/ArrowRight on a top-level item move between
|
|
37
|
+
* top-level items. No wrap at either end. Handled arrows `preventDefault` so the page does not scroll.
|
|
38
|
+
* Hover stays pure theme CSS and never touches `aria-expanded`.
|
|
39
|
+
*/
|
|
40
|
+
export const MENU_DROPDOWN_SCRIPT = `(function(){
|
|
41
|
+
var roots=document.querySelectorAll('[data-jini-dropdown]');
|
|
42
|
+
for(var i=0;i<roots.length;i++)roots[i].classList.add('jini-menu-js');
|
|
43
|
+
function inMenu(el){return el&&el.closest&&el.closest('[data-jini-dropdown]')?el:null;}
|
|
44
|
+
function toggleOf(li){return li.querySelector(':scope > .jini-menu-toggle');}
|
|
45
|
+
function panelOf(li){var b=toggleOf(li);return b?document.getElementById(b.getAttribute('aria-controls')):null;}
|
|
46
|
+
function first(li){return li?li.querySelector('a[href], button'):null;}
|
|
47
|
+
function setOpen(li,open){
|
|
48
|
+
var items=open?[li]:[li].concat(Array.prototype.slice.call(li.querySelectorAll('li.jini-menu-open')));
|
|
49
|
+
for(var i=0;i<items.length;i++){items[i].classList[open?'add':'remove']('jini-menu-open');var b=toggleOf(items[i]);if(b)b.setAttribute('aria-expanded',open?'true':'false');}
|
|
50
|
+
}
|
|
51
|
+
function closeAll(keep){
|
|
52
|
+
var open=document.querySelectorAll('[data-jini-dropdown] li.jini-menu-open');
|
|
53
|
+
for(var i=0;i<open.length;i++)if(!(keep&&keep.nodeType===1&&open[i].contains(keep)))setOpen(open[i],false);
|
|
54
|
+
}
|
|
55
|
+
document.addEventListener('focusin',function(e){closeAll(e.target);});
|
|
56
|
+
document.addEventListener('click',function(e){
|
|
57
|
+
closeAll(e.target);
|
|
58
|
+
var b=inMenu(e.target)?e.target.closest('.jini-menu-toggle'):null;
|
|
59
|
+
if(b)setOpen(b.parentNode,b.getAttribute('aria-expanded')!=='true');
|
|
60
|
+
});
|
|
61
|
+
document.addEventListener('keydown',function(e){
|
|
62
|
+
var t=inMenu(e.target);if(!t)return;
|
|
63
|
+
if(e.key==='Escape'){var o=t.closest('li.jini-menu-open');if(o){setOpen(o,false);toggleOf(o).focus();}return;}
|
|
64
|
+
var li=t.closest('li'),next=null;if(!li)return;
|
|
65
|
+
var top=li.parentNode.hasAttribute('data-jini-dropdown');
|
|
66
|
+
if(e.key==='ArrowDown'&&t.classList.contains('jini-menu-toggle')){closeAll(li);setOpen(li,true);var p=panelOf(li);next=p?first(p):null;}
|
|
67
|
+
else if(!top&&e.key==='ArrowDown')next=first(li.nextElementSibling);
|
|
68
|
+
else if(!top&&e.key==='ArrowUp')next=first(li.previousElementSibling);
|
|
69
|
+
else if(top&&e.key==='ArrowRight')next=first(li.nextElementSibling);
|
|
70
|
+
else if(top&&e.key==='ArrowLeft')next=first(li.previousElementSibling);
|
|
71
|
+
else return;
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
if(next)next.focus();
|
|
74
|
+
});
|
|
75
|
+
})();`;
|
|
76
|
+
/** Adds the one inline dropdown script before `</body>` when the page carries a `data-jini-dropdown`
|
|
77
|
+
* menu and does not already have the script. @complexity O(n). */
|
|
78
|
+
export function injectMenuDropdownScript({ html }, _optional = {}) {
|
|
79
|
+
if (!html.includes('data-jini-dropdown') || html.includes(SCRIPT_MARKER))
|
|
80
|
+
return html;
|
|
81
|
+
const script = `<script ${SCRIPT_MARKER}>${MENU_DROPDOWN_SCRIPT}</script>`;
|
|
82
|
+
const bodyClose = html.lastIndexOf('</body>');
|
|
83
|
+
return bodyClose === -1 ? html + script : html.slice(0, bodyClose) + script + html.slice(bodyClose);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=dropdown.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A module's public contract is its `index.ts` — deep imports
|
|
5
5
|
* from outside this directory should go through here.
|
|
6
6
|
*/
|
|
7
|
-
export type { NavTargetKind, ReservedNavTargetKind, NavTarget, NavEntryTarget, NavTermTarget, NavUrlTarget, NavRouteTarget, NavItemAttrs, NavItemNode, NavMenuDoc, NavMenuEntry, MenuStatus, NavLocationKey, NavLocationBindingRow, NavLocationDescriptor, ResolvedNavItem, ResolvedNav, } from "./types.js";
|
|
7
|
+
export type { NavTargetKind, ReservedNavTargetKind, NavTarget, NavEntryTarget, NavTermTarget, NavUrlTarget, NavRouteTarget, NavItemAttrs, NavItemNode, NavMenuDoc, NavMenuMode, NavMenuEntry, MenuStatus, NavLocationKey, NavLocationBindingRow, NavLocationDescriptor, ResolvedNavItem, ResolvedNav, } from "./types.js";
|
|
8
8
|
export { NAV_MENU_CONTENT_TYPE, NAV_FIELD_NAMESPACE, NAV_DOC_TYPE } from "./types.js";
|
|
9
9
|
export type { NavLocationBindingRepoPort, NavResolveContext, NavTargetResolver, NavMenuReadModel, NavLocationRegistry, } from "./ports.js";
|
|
10
10
|
export type { NavigationPermission, CreateMenuInput, UpdateMenuInput, AssignLocationInput, UnassignLocationInput, DeleteMenuInput, NavigationEventName, NavMenuChangedPayload, NavLocationChangedPayload, NavigationHookName, NavigationAiTool, NavWhereUsedResult, } from "./contracts.js";
|
|
@@ -26,8 +26,12 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, menuVersionConflictEr
|
|
|
26
26
|
* `check:boundaries` blocks either host-side call site from doing so.
|
|
27
27
|
*/
|
|
28
28
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuVersionConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, type TreeValidationLimits, type CreateMenuDeps, type CreateMenuServiceInput, type CreateMenuRequired, type CreateMenuOptional, type UpdateMenuTreeDeps, type UpdateMenuTreeServiceInput, type UpdateMenuTreeRequired, type UpdateMenuTreeOptional, type AssignLocationDeps, type AssignLocationServiceInput, type AssignLocationRequired, type AssignLocationOptional, type DeleteMenuDeps, type DeleteMenuServiceInput, type DeleteMenuRequired, type DeleteMenuOptional, } from "./menu-service.js";
|
|
29
|
+
/** HTML mode (2026-10-08): validation/normalization for a menu's author-written markup. */
|
|
30
|
+
export { MAX_MENU_HTML_LENGTH, isMenuHtmlAuthoring, menuHtmlInputProblem, mergeMenuHtml, type MenuHtmlAuthoring, } from "./menu-html.js";
|
|
29
31
|
export { resolveForLocation, resolveMenuDoc, type ResolveTargetHrefFn, type ResolvedTargetHref, type ResolveForLocationDeps, type ResolveForLocationServiceInput, type ResolveForLocationRequired, type ResolveForLocationOptional, } from "./resolver.js";
|
|
30
32
|
export { rebuildNavLocationBindings, type RebuildNavLocationBindingsDeps, type RebuildNavLocationBindingsResult, } from "./reconcile.js";
|
|
33
|
+
/** Dropdown menus (2026-10-11): toggle markup + the inline keyboard/touch script (see `dropdown.ts`). */
|
|
34
|
+
export { renderMenuToggle, injectMenuDropdownScript, MENU_DROPDOWN_SCRIPT } from "./dropdown.js";
|
|
31
35
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
32
36
|
export { menusAgentToolCatalog, } from "./agent-tools.js";
|
|
33
37
|
/**
|
package/dist/navigation/index.js
CHANGED
|
@@ -17,8 +17,12 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, menuVersionConflictEr
|
|
|
17
17
|
* `check:boundaries` blocks either host-side call site from doing so.
|
|
18
18
|
*/
|
|
19
19
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuVersionConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, } from "./menu-service.js";
|
|
20
|
+
/** HTML mode (2026-10-08): validation/normalization for a menu's author-written markup. */
|
|
21
|
+
export { MAX_MENU_HTML_LENGTH, isMenuHtmlAuthoring, menuHtmlInputProblem, mergeMenuHtml, } from "./menu-html.js";
|
|
20
22
|
export { resolveForLocation, resolveMenuDoc, } from "./resolver.js";
|
|
21
23
|
export { rebuildNavLocationBindings, } from "./reconcile.js";
|
|
24
|
+
/** Dropdown menus (2026-10-11): toggle markup + the inline keyboard/touch script (see `dropdown.ts`). */
|
|
25
|
+
export { renderMenuToggle, injectMenuDropdownScript, MENU_DROPDOWN_SCRIPT } from "./dropdown.js";
|
|
22
26
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
23
27
|
export { menusAgentToolCatalog, } from "./agent-tools.js";
|
|
24
28
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NavMenuDoc, NavMenuMode } from "./types.js";
|
|
2
|
+
/** Same ceiling HTML-mode forms use. */
|
|
3
|
+
export declare const MAX_MENU_HTML_LENGTH = 200000;
|
|
4
|
+
/** The optional HTML-mode fields a create/update write may carry. */
|
|
5
|
+
export interface MenuHtmlAuthoring {
|
|
6
|
+
mode?: NavMenuMode | undefined;
|
|
7
|
+
html?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether a write authors raw HTML, and so needs the host's raw-HTML permission. Switching back to
|
|
11
|
+
* items without sending markup does not author anything (forms apply the same rule to "builder").
|
|
12
|
+
* @complexity O(1).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isMenuHtmlAuthoring({ mode, html }: MenuHtmlAuthoring, _optional?: Record<string, never>): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Why a write's `mode`/`html` is invalid, or `null` when it is fine. A message rather than a throw so
|
|
17
|
+
* `menu-service.ts` raises its own `MenuValidationError` without this file importing it back.
|
|
18
|
+
* @complexity O(1).
|
|
19
|
+
*/
|
|
20
|
+
export declare function menuHtmlInputProblem({ mode, html }: MenuHtmlAuthoring, _optional?: Record<string, never>): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The doc's `mode`/`html` after a valid write: submitted values win, omitted ones keep what is
|
|
23
|
+
* stored, and a menu that never used HTML mode gets neither key, so its stored doc (and publish
|
|
24
|
+
* hash) is unchanged. Call {@link menuHtmlInputProblem} first.
|
|
25
|
+
* @complexity O(h) in the submitted html length (one parse + serialize).
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeMenuHtml({ input, existing }: {
|
|
28
|
+
input: MenuHtmlAuthoring;
|
|
29
|
+
existing?: Pick<NavMenuDoc, "mode" | "html"> | undefined;
|
|
30
|
+
}, _optional?: Record<string, never>): Pick<NavMenuDoc, "mode" | "html">;
|
|
31
|
+
//# sourceMappingURL=menu-html.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file HTML mode for a menu (2026-10-08): the author writes the menu's markup by hand instead of
|
|
3
|
+
* building an item tree, so it can be styled however they want.
|
|
4
|
+
*
|
|
5
|
+
* Same trust model as HTML-mode forms (`forms/html/html-authoring.ts`) and HTML Pages: the markup is
|
|
6
|
+
* admin/owner-trusted and NOT sanitized. Hosts gate it behind their raw-HTML permission (Tovu binds
|
|
7
|
+
* `pages.edit_html`, the one forms use); this module only validates and normalizes. parse5 balances
|
|
8
|
+
* the fragment so a stray `</nav>` or an unclosed `<div>` cannot close or swallow the theme element
|
|
9
|
+
* the menu renders inside.
|
|
10
|
+
*
|
|
11
|
+
* Both modes' data live side by side in the doc: switching mode never deletes the other one, so an
|
|
12
|
+
* author can flip back and find their items (or their HTML) still there.
|
|
13
|
+
*/
|
|
14
|
+
import { parseFragment, serialize } from "parse5";
|
|
15
|
+
/** Same ceiling HTML-mode forms use. */
|
|
16
|
+
export const MAX_MENU_HTML_LENGTH = 200_000;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a write authors raw HTML, and so needs the host's raw-HTML permission. Switching back to
|
|
19
|
+
* items without sending markup does not author anything (forms apply the same rule to "builder").
|
|
20
|
+
* @complexity O(1).
|
|
21
|
+
*/
|
|
22
|
+
export function isMenuHtmlAuthoring({ mode, html }, _optional = {}) {
|
|
23
|
+
return html !== undefined || mode === "html";
|
|
24
|
+
}
|
|
25
|
+
/** Balance the fragment; scripts and authored styling stay as written (trusted markup). */
|
|
26
|
+
function normalizeMenuHtml(html) {
|
|
27
|
+
return serialize(parseFragment(html));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Why a write's `mode`/`html` is invalid, or `null` when it is fine. A message rather than a throw so
|
|
31
|
+
* `menu-service.ts` raises its own `MenuValidationError` without this file importing it back.
|
|
32
|
+
* @complexity O(1).
|
|
33
|
+
*/
|
|
34
|
+
export function menuHtmlInputProblem({ mode, html }, _optional = {}) {
|
|
35
|
+
if (mode !== undefined && mode !== "items" && mode !== "html")
|
|
36
|
+
return "mode must be 'items' or 'html'";
|
|
37
|
+
if (html !== undefined && (typeof html !== "string" || html.length > MAX_MENU_HTML_LENGTH)) {
|
|
38
|
+
return `html must be a string of at most ${MAX_MENU_HTML_LENGTH} characters`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The doc's `mode`/`html` after a valid write: submitted values win, omitted ones keep what is
|
|
44
|
+
* stored, and a menu that never used HTML mode gets neither key, so its stored doc (and publish
|
|
45
|
+
* hash) is unchanged. Call {@link menuHtmlInputProblem} first.
|
|
46
|
+
* @complexity O(h) in the submitted html length (one parse + serialize).
|
|
47
|
+
*/
|
|
48
|
+
export function mergeMenuHtml({ input, existing }, _optional = {}) {
|
|
49
|
+
const mode = input.mode ?? existing?.mode;
|
|
50
|
+
const html = input.html !== undefined ? normalizeMenuHtml(input.html) : existing?.html;
|
|
51
|
+
return { ...(mode !== undefined ? { mode } : {}), ...(html !== undefined ? { html } : {}) };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=menu-html.js.map
|
|
@@ -2,7 +2,7 @@ import type { Clock, IdGenerator, UUID } from "@jini-ai/core/primitives";
|
|
|
2
2
|
import type { OutboxPort } from "../core/ports.js";
|
|
3
3
|
import type { MenuRepoPort } from "./repo.memory.js";
|
|
4
4
|
import type { NavLocationBindingRepoPort } from "./ports.js";
|
|
5
|
-
import { type NavItemNode, type NavLocationBindingRow, type NavLocationKey, type NavMenuEntry } from "./types.js";
|
|
5
|
+
import { type NavItemNode, type NavLocationBindingRow, type NavLocationKey, type NavMenuEntry, type NavMenuMode } from "./types.js";
|
|
6
6
|
export declare class MenuNotFoundError extends Error {
|
|
7
7
|
constructor({ message }: {
|
|
8
8
|
message: string;
|
|
@@ -147,6 +147,9 @@ export interface CreateMenuServiceInput {
|
|
|
147
147
|
slug: string;
|
|
148
148
|
/** Optional initial tree; defaults to an empty menu. Items must carry ids. */
|
|
149
149
|
items?: readonly NavItemNode[] | undefined;
|
|
150
|
+
/** HTML mode (`menu-html.ts`). The host gates raw HTML behind its own permission first. */
|
|
151
|
+
mode?: NavMenuMode | undefined;
|
|
152
|
+
html?: string | undefined;
|
|
150
153
|
}
|
|
151
154
|
export interface CreateMenuRequired {
|
|
152
155
|
deps: CreateMenuDeps;
|
|
@@ -188,8 +191,11 @@ export interface UpdateMenuTreeServiceInput {
|
|
|
188
191
|
expectedVersion: number;
|
|
189
192
|
title?: string | undefined;
|
|
190
193
|
slug?: string | undefined;
|
|
191
|
-
/** The full replacement tree (whole-tree edit). */
|
|
192
|
-
items
|
|
194
|
+
/** The full replacement tree (whole-tree edit). Omitted keeps the stored tree (an HTML-only edit). */
|
|
195
|
+
items?: readonly NavItemNode[] | undefined;
|
|
196
|
+
/** HTML mode (`menu-html.ts`); omitted keeps the stored value. The host gates raw HTML first. */
|
|
197
|
+
mode?: NavMenuMode | undefined;
|
|
198
|
+
html?: string | undefined;
|
|
193
199
|
}
|
|
194
200
|
export interface UpdateMenuTreeRequired {
|
|
195
201
|
deps: UpdateMenuTreeDeps;
|
|
@@ -24,6 +24,7 @@ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
|
24
24
|
* through the injected repo ports (`deps`).
|
|
25
25
|
*/
|
|
26
26
|
import { assertEntityLive } from "../core/entity-liveness.js";
|
|
27
|
+
import { menuHtmlInputProblem, mergeMenuHtml } from "./menu-html.js";
|
|
27
28
|
import { NAV_DOC_TYPE, } from "./types.js";
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Outbox event publication — each mutating function below
|
|
@@ -281,6 +282,13 @@ function assertValidTitleAndSlug(title, slug) {
|
|
|
281
282
|
throw new MenuValidationError({ message: "slug must use lowercase letters, numbers, and dashes" });
|
|
282
283
|
}
|
|
283
284
|
}
|
|
285
|
+
/** Validated, merged HTML-mode doc fields — `menu-html.ts`'s problem check raised as this file's own error. */
|
|
286
|
+
function menuHtmlFields({ input, existing }) {
|
|
287
|
+
const problem = menuHtmlInputProblem(input);
|
|
288
|
+
if (problem)
|
|
289
|
+
throw new MenuValidationError({ message: problem });
|
|
290
|
+
return mergeMenuHtml({ input, existing });
|
|
291
|
+
}
|
|
284
292
|
/**
|
|
285
293
|
* Creates a new `menu` entry in `published` status with no location
|
|
286
294
|
* assignments. Rejects a duplicate slug within the workspace.
|
|
@@ -305,13 +313,14 @@ export async function createMenu(required, optional = {}) {
|
|
|
305
313
|
if (duplicate)
|
|
306
314
|
throw new MenuConflictError({ message: `slug '${slug}' already exists` });
|
|
307
315
|
const items = validateAndCloneTree({ items: input.items ?? [] }, optional.limits);
|
|
316
|
+
const htmlFields = menuHtmlFields({ input });
|
|
308
317
|
const menu = {
|
|
309
318
|
id: deps.idGen.newId(),
|
|
310
319
|
workspaceId: input.workspaceId,
|
|
311
320
|
slug,
|
|
312
321
|
title,
|
|
313
322
|
status: "published",
|
|
314
|
-
doc: { type: NAV_DOC_TYPE, version: 1, items },
|
|
323
|
+
doc: { type: NAV_DOC_TYPE, version: 1, items, ...htmlFields },
|
|
315
324
|
locations: [],
|
|
316
325
|
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
317
326
|
version: 1,
|
|
@@ -407,15 +416,18 @@ export async function updateMenuTree(required, optional = {}) {
|
|
|
407
416
|
throw new MenuConflictError({ message: `slug '${slug}' already exists` });
|
|
408
417
|
}
|
|
409
418
|
}
|
|
410
|
-
const items = carryForwardEntryHints({
|
|
419
|
+
const items = input.items === undefined ? existing.doc.items : carryForwardEntryHints({
|
|
411
420
|
previous: existing.doc.items,
|
|
412
421
|
next: validateAndCloneTree({ items: input.items }, optional.limits),
|
|
413
422
|
});
|
|
423
|
+
// Every writer (admin, agent tool, publish repoint, reverters) comes through here, so the stored
|
|
424
|
+
// HTML-mode fields are carried forward unless this write sets them — an items-only save never drops them.
|
|
425
|
+
const htmlFields = menuHtmlFields({ input, existing: existing.doc });
|
|
414
426
|
const menu = {
|
|
415
427
|
...existing,
|
|
416
428
|
title,
|
|
417
429
|
slug,
|
|
418
|
-
doc: { type: NAV_DOC_TYPE, version: existing.doc.version, items },
|
|
430
|
+
doc: { type: NAV_DOC_TYPE, version: existing.doc.version, items, ...htmlFields },
|
|
419
431
|
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
420
432
|
version: existing.version + 1,
|
|
421
433
|
};
|