@valbuild/server 0.140.2 → 0.141.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.141.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#808](https://github.com/valbuild/val/pull/808) [`14b18d7`](https://github.com/valbuild/val/commit/14b18d7cb145cec1bc1cd832b9ca857ff4b45adc) Thanks [@freekh](https://github.com/freekh)! - A project with `files: { remote: true }` can save text edits without remote-file credentials: in local development without `val login` or a project id, and on a server without `VAL_API_KEY`.
8
+
9
+ Before, a save asked for remote credentials whenever any schema in the project was remote. With `files: { remote: true }` that is every media schema, so every save was refused, even a typo fix. Now it asks only when the change being saved contains a remote file.
10
+
11
+ When uploads are unavailable, the Studio now shows a card above the editor. It says what is wrong and gives the fix: a link to admin.val.build or the `val login` command to copy. When the cause is missing setup (no project id, not logged in, no API key) it also says that text edits still save and publish. Dismissing it leaves an "Uploads off" item in the status bar, or in the account sheet on a phone, that brings it back. This replaces the red bar that covered the top of the Studio, the Publish button included, and the login dialog that opened on every load.
12
+
13
+ - Updated dependencies [[`6faacda`](https://github.com/valbuild/val/commit/6faacda398fc92bec187b7afe07a53228a6f2a3b), [`14b18d7`](https://github.com/valbuild/val/commit/14b18d7cb145cec1bc1cd832b9ca857ff4b45adc)]:
14
+ - @valbuild/ui@0.141.1
15
+
16
+ ## 0.141.0
17
+
18
+ ### Minor Changes
19
+
20
+ - [#802](https://github.com/valbuild/val/pull/802) [`199e214`](https://github.com/valbuild/val/commit/199e21471d98806fb34c579cb876560078c928b9) Thanks [@freekh](https://github.com/freekh)! - New: `files: { remote: true }` in `val.config.ts` stores every image, file and video on Val's remote content host.
21
+
22
+ ```ts
23
+ const { s, c, val, config } = initVal({
24
+ project: "myorg/myproject",
25
+ files: { remote: true },
26
+ });
27
+ ```
28
+
29
+ With it, `s.image()`, `s.file()`, `s.video()`, `s.imageset()`, `s.fileset()`, `s.videoset()` and `s.richtext({ img: true })` behave as though `.remote()` had been written on them. Images already in the project are reported as `image:upload-remote` until they are uploaded; `npx val validate --fix` does that.
30
+
31
+ **Required in the Val app.** A project running there (`VAL_ENV=app`) refuses to start without it, with an error that says what to add, and `val publish` refuses before it builds.
32
+
33
+ **Breaking:** `files.directory` has been removed from `val.config.ts`. Local files that have no `dir` of their own are stored in `/public/val`, which was the default.
34
+
35
+ - [#779](https://github.com/valbuild/val/pull/779) [`ba3f330`](https://github.com/valbuild/val/commit/ba3f3300a3dd8649e154097cdfca3b64db209d89) Thanks [@freekh](https://github.com/freekh)! - New: `s.video()`, for mp4/WebM files and HLS streams.
36
+
37
+ ```ts
38
+ const schema = s.object({
39
+ intro: s.video({ stream: { type: "hls" } }),
40
+ });
41
+ ```
42
+
43
+ - **Upload and play in the Studio.** A video field shows a player, the file's size and length, and progress while it uploads.
44
+ - **HLS streaming without a video service.** With `stream: { type: "hls" }`, the Studio converts an upload to an HLS stream in the editor's browser before it uploads it. It makes one H.264 rendition per height (1080p, 720p and 480p by default, and never larger than the upload) and stores them as a folder of files next to your other uploads. Nothing is installed in your app for this: the converter ships inside the Studio and is only downloaded when someone uploads to a streaming field. A browser that can't convert (no WebCodecs, or a non-https address) uploads the original file and says so.
45
+ - **Poster:** pause on a frame and choose "Use current frame". The frame is saved as an image, and `posterTime` records where it was taken.
46
+ - **Start and end:** play part of a video without cutting the file.
47
+ - **Focal point:** marks what must stay in frame when the page crops the video.
48
+ - **Description:** text for people who can't see the video.
49
+ - **Captions:** one WebVTT file per language. `.srt` files are converted on upload.
50
+ - **`<ValVideo>`** in `@valbuild/next` and `@valbuild/tanstack` renders all of the above and is click-to-edit. For HLS in browsers that can't play it natively, pass `hls={() => import("hls.js")}`. It is only loaded when it's needed.
51
+ - **Rename** a video in the Studio. An HLS stream is renamed as a whole: every playlist and segment moves to the new folder.
52
+ - `npx val validate --fix` fills in `mimeType`, `width`, `height` and `duration` for a hand-written `.mp4`, `.mov`, `.webm`, `.mkv` or HLS video, on disk or on Val Remote. A remote video is not downloaded: only its headers are read, usually in one to three small requests.
53
+ - `.remote()` works for videos. `npx val validate --fix` moves a video to Val Remote, or back into the project, together with its poster, captions and every file of an HLS stream.
54
+
55
+ A video value always has a `mimeType`: it is how a page knows whether it has an `.mp4` or an `.m3u8`.
56
+
57
+ New: `s.videoset()`, a collection of videos, like `s.imageset()` for images.
58
+
59
+ ```ts
60
+ const videosVal = c.define(
61
+ "/content/videos.val.ts",
62
+ s.videoset({ dir: "/public/val/videos", stream: { type: "hls" } }),
63
+ {},
64
+ );
65
+ const schema = s.object({ intro: s.video(videosVal) });
66
+ ```
67
+
68
+ - **Upload once, set it up once, use anywhere.** A set's entry holds what is true of the file (its type, size and length) and, as defaults, a description, poster, start and end, focal point and captions. A field that picks from the set (`s.video(videosVal)`) gets all of those, and can override any of them on its own. For example, the same clip can open one page at 0:02 and every other page where the set says.
69
+ - **Media in the Studio.** A set is listed under Media and opens as a gallery of videos, with each video's poster as its thumbnail. Hovering a video previews it, and the selected video plays in a real player (HLS streams too) beside the poster, times, focal point and captions you edit for it. You can upload into the set (with `stream`, uploads become HLS streams, and each upload gets a poster), rename a video, and delete a video that nothing uses. Renaming a video updates every field that uses it. An HLS stream is one entry, and renaming or deleting it moves or removes all of its files.
70
+ - **Picking.** A set-backed field picks from the set, or uploads into it. Each value the set provides is marked "From gallery · Override", and once overridden, "Overridden · Use gallery's".
71
+ - `useVal` and `fetchVal` fill in what the set holds (`mimeType`, `width`, `height`, `duration`, and every default the field does not override), so `<ValVideo>` works the same either way.
72
+ - `.remote()` works for sets. `npx val validate --fix` fills in missing metadata for an entry, adds untracked videos found in the set's directory, and moves a set's videos to Val Remote.
73
+
74
+ Image and file galleries use the same new gallery layout: a grid or list of entries, with the selected one open in a panel beside it.
75
+
76
+ - An `s.imageset()` entry can hold a default focal point, as well as its description. An `s.image(galleryVal)` field shows both, and can override either.
77
+ - Fixed: outside draft mode, `useVal` and `fetchVal` now fill a gallery-backed image or file from its gallery (dimensions, mime type, description). Before, a published page got only the `path`.
78
+
79
+ ### Patch Changes
80
+
81
+ - Updated dependencies [[`199e214`](https://github.com/valbuild/val/commit/199e21471d98806fb34c579cb876560078c928b9), [`9cbe428`](https://github.com/valbuild/val/commit/9cbe42895d006d55c3d993dc27f0779808b85306), [`fb5f097`](https://github.com/valbuild/val/commit/fb5f09721ac07f4d90b8502fdbee3fb3884d9688), [`6473a2e`](https://github.com/valbuild/val/commit/6473a2efe874f1749b27ed6f8ac4e0d19805edb1), [`ba3f330`](https://github.com/valbuild/val/commit/ba3f3300a3dd8649e154097cdfca3b64db209d89)]:
82
+ - @valbuild/core@0.141.0
83
+ - @valbuild/shared@0.141.0
84
+ - @valbuild/ui@0.141.0
85
+
3
86
  ## 0.140.2
4
87
 
5
88
  ### Patch Changes
@@ -594,6 +594,19 @@ export declare abstract class ValOps {
594
594
  abstract getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId, remote: boolean): Promise<Buffer | null>;
595
595
  protected abstract getBase64EncodedBinaryFileMetadataFromPatch<T extends "file" | "image">(filePath: string, type: T, patchId: PatchId, remote: boolean): Promise<OpsMetadata<T>>;
596
596
  abstract getBinaryFile(filePathOrRef: string): Promise<Buffer | null>;
597
+ /**
598
+ * A file's bytes as something a `Range` request can be answered from
599
+ * without reading the rest: a video is seeked in by many small ranges, and
600
+ * each used to load the whole file.
601
+ *
602
+ * This default holds the whole file, which is all a mode that only gets
603
+ * whole files can do (`ValOpsHttp`: the content service answers with the
604
+ * file in a JSON body). `ValOpsFS` reads only the asked-for range off disk.
605
+ */
606
+ openBinaryFile(filePath: string, fromPatch: {
607
+ patchId: PatchId;
608
+ remote: boolean;
609
+ } | null): Promise<BinaryFileReader | null>;
597
610
  protected abstract getBinaryFileMetadata<T extends "file" | "image">(filePath: string, type: T): Promise<OpsMetadata<T>>;
598
611
  abstract deletePatches(patchIds: PatchId[]): Promise<{
599
612
  deleted: PatchId[];
@@ -977,3 +990,10 @@ export declare function getFieldsForType<T extends BinaryFileType>(type: T): (ke
977
990
  export declare function createMetadataFromBuffer<T extends BinaryFileType>(type: BinaryFileType, mimeType: string, buffer: Buffer): OpsMetadata<T>;
978
991
  export declare function guessMimeTypeFromPath(filePath: string): string | null;
979
992
  export declare function bufferFromDataUrl(dataUrl: string): Buffer | undefined;
993
+ /** A file of known size whose bytes are read a range at a time. */
994
+ export type BinaryFileReader = {
995
+ readonly size: number;
996
+ /** Bytes `start` to `end`, both inclusive, as an HTTP range is. */
997
+ read(start: number, end: number): Promise<Buffer>;
998
+ };
999
+ export declare function bufferReader(buffer: Buffer): BinaryFileReader;
@@ -1,5 +1,5 @@
1
1
  import { PatchId, ModuleFilePath, ValModules } from "@valbuild/core";
2
- import { AuthorId, BaseSha, BinaryFileType, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, type PatchGroupMembership, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, SourcesSha } from "./ValOps.js";
2
+ import { AuthorId, BaseSha, BinaryFileType, BinaryFileReader, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, type PatchGroupMembership, SchemaSha, CommitSha, OrderedPatches, OrderedPatchesMetadata, SourcesSha } from "./ValOps.js";
3
3
  import { Patch, ParentRef, ValCommit } from "@valbuild/shared/internal";
4
4
  import type { HistoryError } from "./history/HistoryError.js";
5
5
  import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
@@ -162,6 +162,10 @@ export declare class ValOpsFS extends ValOps {
162
162
  filePath?: string;
163
163
  }>;
164
164
  }>;
165
+ openBinaryFile(filePath: string, fromPatch: {
166
+ patchId: PatchId;
167
+ remote: boolean;
168
+ } | null): Promise<BinaryFileReader | null>;
165
169
  getBinaryFile(filePath: string): Promise<Buffer | null>;
166
170
  protected getBinaryFileMetadata<T extends BinaryFileType>(filePath: string, type: T): Promise<OpsMetadata<T>>;
167
171
  private getPatchesDir;
@@ -1,4 +1,4 @@
1
- import { FileMetadata, ImageMetadata, SerializedSchema, Source, SourcePath, ValidationError } from "@valbuild/core";
1
+ import { FileMetadata, ImageMetadata, VideoMetadata, SerializedSchema, Source, SourcePath, ValidationError } from "@valbuild/core";
2
2
  import { JSONValue, Patch } from "@valbuild/core/patch";
3
3
  export type FixPatchRemainingError = ValidationError & {
4
4
  sourcePath?: SourcePath;
@@ -21,10 +21,12 @@ export declare function createFixPatch(config: {
21
21
  [sourcePath: SourcePath]: {
22
22
  ref: string;
23
23
  metadata?: Record<string, unknown>;
24
+ refs?: Record<string, string>;
24
25
  };
25
26
  }, moduleSource?: Source, moduleSchema?: SerializedSchema): Promise<{
26
27
  patch: Patch;
27
28
  remainingErrors: FixPatchRemainingError[];
28
29
  } | undefined>;
30
+ export declare function getVideoMetadata(projectRoot: string, fileRef: string): Promise<VideoMetadata>;
29
31
  export declare function getImageMetadata(projectRoot: string, validationError: ValidationError): Promise<ImageMetadata>;
30
32
  export declare function getFileMetadata(projectRoot: string, validationError: ValidationError): Promise<FileMetadata>;
@@ -1,4 +1,50 @@
1
- import { FileMetadata, ImageMetadata } from "@valbuild/core";
1
+ import { FileMetadata, ImageMetadata, VideoMetadata } from "@valbuild/core";
2
2
  import { Buffer } from "buffer";
3
+ import { type ByteSource } from "./isoBmff.js";
3
4
  export declare function extractImageMetadata(filename: string, input: Buffer): Promise<ImageMetadata>;
4
5
  export declare function extractFileMetadata(filename: string, _input: Buffer): Promise<FileMetadata>;
6
+ /**
7
+ * What `val validate --fix` writes into a video: `mimeType`, `width`, `height`
8
+ * and `duration`, read from the file. A field that cannot be read is left out
9
+ * rather than guessed, and the caller reports it — see
10
+ * {@link unreadableVideoMetadataMessage}.
11
+ *
12
+ * `mimeType` always comes from the file EXTENSION: validation compares the two,
13
+ * so any other answer is one validation rejects. The rest depends on the kind
14
+ * of file:
15
+ *
16
+ * - **ISO BMFF** (`.mp4`, `.m4v`, `.mov`): read from the `moov` box by a small
17
+ * parser of our own (`isoBmff.ts`). No media library: this package is loaded
18
+ * by every app that runs Val.
19
+ * - **WebM / Matroska** (`.webm`, `.mkv`): read from the Segment's Info and Tracks by another
20
+ * (`ebml.ts`). A WebM recorded in a browser does not declare its length, and
21
+ * then `duration` is left out — the frames are not counted to find it.
22
+ * - **An HLS master playlist** (`.m3u8`): the dimensions are the largest
23
+ * rendition's `RESOLUTION`, and the duration is the sum of the `#EXTINF`s of
24
+ * the first media playlist it names, read from disk beside it — which is why
25
+ * `filename` must be the playlist's path on disk.
26
+ * - **Anything else**: the mime type and nothing more. The Studio reads those
27
+ * in the browser when they are uploaded.
28
+ */
29
+ export declare function extractVideoMetadata(filename: string, input: Buffer, readFile?: (absolutePath: string) => Buffer | undefined): Promise<VideoMetadata>;
30
+ /**
31
+ * {@link extractVideoMetadata} for a file on disk, reading only the headers
32
+ * it needs: a video's frames (an mp4's `mdat`, a WebM's Clusters) are most of
33
+ * the file, and are skipped, not loaded.
34
+ */
35
+ export declare function extractVideoMetadataFromFile(absolutePath: string): Promise<VideoMetadata>;
36
+ /**
37
+ * What to tell someone whose video's `fields` could not be read. Names the
38
+ * way out, because "could not read" alone leaves them nowhere to go.
39
+ */
40
+ export declare function unreadableVideoMetadataMessage(fileRef: string, fields: readonly string[]): string;
41
+ /** Whether Val reads this video's size and length itself, by its extension. */
42
+ export declare function canReadVideoMetadata(fileRef: string): boolean;
43
+ export declare function extractFromByteSource(filename: string, source: ByteSource): VideoMetadata;
44
+ /**
45
+ * An HLS stream's metadata from its master playlist's text, and — for the
46
+ * duration — the first media playlist it names, which `readMediaPlaylist`
47
+ * fetches by the URI the master gives (relative to it, or a remote ref). The
48
+ * one implementation for a stream on disk and one on Val Remote.
49
+ */
50
+ export declare function readHlsMetadata(text: string, readMediaPlaylist: (uri: string) => Promise<string | undefined>): Promise<VideoMetadata>;
@@ -27,9 +27,12 @@
27
27
  * walks every module, decides what to report, and renders it to a terminal.
28
28
  */
29
29
  import { ModuleFilePath, SourcePath, ValidationFix } from "@valbuild/core";
30
+ import { checkGalleryFiles } from "./galleryFiles.js";
30
31
  import type { Service } from "./Service.js";
31
32
  import type { IValFSHost } from "./ValFSHost.js";
33
+ import type { Patch } from "@valbuild/core/patch";
32
34
  export type { IValFSHost };
35
+ export { checkGalleryFiles };
33
36
  export type IValRemote = {
34
37
  remoteHost: string;
35
38
  getSettings(projectName: string, options: {
@@ -73,27 +76,49 @@ export type FixHandlerContext = {
73
76
  moduleFilePath: ModuleFilePath;
74
77
  file: string;
75
78
  fs: IValFSHost;
76
- remoteFiles: Record<SourcePath, {
77
- ref: string;
78
- metadata?: Record<string, unknown>;
79
- }>;
79
+ remoteFiles: Record<SourcePath, RemoteFileMove>;
80
80
  publicProjectId?: string;
81
81
  remoteFileBuckets?: string[];
82
82
  remoteFilesCounter: number;
83
83
  remote: IValRemote;
84
84
  project: string | undefined;
85
85
  };
86
+ /**
87
+ * Where a fix moved a media value's file. `refs` is for a value that names
88
+ * SEVERAL files — a video's poster, captions and stream — keyed by the path
89
+ * the value held, so the patch can rewrite each one where it is.
90
+ */
91
+ export type RemoteFileMove = {
92
+ ref: string;
93
+ metadata?: Record<string, unknown>;
94
+ refs?: Record<string, string>;
95
+ };
86
96
  export type FixHandlerResult = {
87
97
  success: boolean;
88
98
  errorMessage?: string;
89
99
  shouldApplyPatch?: boolean;
90
100
  appliedFix?: boolean;
91
101
  fixableErrorMessage?: string;
102
+ /**
103
+ * Patches to OTHER modules than the one the fix is about, for the caller to
104
+ * apply alongside the fix's own patch (and only when it applies that one).
105
+ *
106
+ * A fix normally rewrites one value. Renaming a key that other modules name
107
+ * cannot be one: `videos:upload-remote` renames a set entry's key to its
108
+ * remote ref, and every `s.video(set)` field holding the old key would name
109
+ * a video the set no longer has.
110
+ */
111
+ otherModulePatches?: ModulePatch[];
92
112
  publicProjectId?: string;
93
113
  remoteFileBuckets?: string[];
94
114
  remoteFilesCounter?: number;
95
115
  events?: ValidationEvent[];
96
116
  };
117
+ /** A patch, and the module it is for. */
118
+ export type ModulePatch = {
119
+ moduleFilePath: ModuleFilePath;
120
+ patch: Patch;
121
+ };
97
122
  export type FixHandler = (ctx: FixHandlerContext) => Promise<FixHandlerResult>;
98
123
  export type ValidationEvent = {
99
124
  type: "file-valid";
@@ -157,40 +182,20 @@ export type ValidationEvent = {
157
182
  type: "summary-success";
158
183
  };
159
184
  export declare function handleFileMetadata(ctx: FixHandlerContext): Promise<FixHandlerResult>;
185
+ /**
186
+ * `video:add-metadata`: the bytes must be on disk, or on Val Remote (read
187
+ * over HTTP, headers only — see `remoteVideoMetadata.ts`), and they must be a
188
+ * kind of file Val can read the size and length of here: mp4/mov boxes,
189
+ * WebM/Matroska headers and HLS playlists. Anything else is refused with what
190
+ * to do instead, unless all that is missing is the mime type, which the
191
+ * extension answers.
192
+ */
193
+ export declare function handleVideoMetadata(ctx: FixHandlerContext): Promise<FixHandlerResult>;
160
194
  export declare function handleRemoteFileUpload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
161
195
  export declare function handleRemoteGalleryFileUpload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
162
196
  export declare function handleRemoteFileDownload(ctx: FixHandlerContext): Promise<FixHandlerResult>;
163
197
  export declare function handleRemoteFileCheck(): Promise<FixHandlerResult>;
164
198
  export declare function handleUniqueFolderCheck(ctx: FixHandlerContext): Promise<FixHandlerResult>;
165
- /**
166
- * What is out of step between a gallery's entries and its directory.
167
- *
168
- * Two questions, and they treat a remote entry differently — which is the whole
169
- * reason this is separate from the handler around it:
170
- *
171
- * - **Missing**: an entry with no bytes at its local path. Asked of LOCAL
172
- * entries only. A remote entry's bytes live on the content host, and nothing
173
- * puts a copy in the working tree: `saveOrUploadFiles` uploads the remote
174
- * descriptors and copies only the local ones into the tree, so a remote entry
175
- * added through the Studio (or over MCP) has no local file by design, and
176
- * demanding one would mean committing remote bytes to git — which is what
177
- * remote storage exists to avoid. Whether those bytes really are on the host
178
- * is a different check, `image:check-remote`, which already runs for exactly
179
- * these entries.
180
- * - **Untracked**: a file in the directory that no entry claims. Asked of every
181
- * entry, remote included, and that is why they are normalised to their local
182
- * path: `val validate --fix` promotes a local file to a remote ref and leaves
183
- * the file where it was, so a remote entry can perfectly well have one.
184
- */
185
- export declare function checkGalleryFiles(input: {
186
- entryKeys: string[];
187
- dir: string;
188
- projectRoot: string;
189
- fs: Pick<IValFSHost, "fileExists" | "readDirectory">;
190
- }): {
191
- missingTrackedFiles: string[];
192
- untrackedFiles: string[];
193
- };
194
199
  export declare function handleCheckAllFiles(ctx: FixHandlerContext): Promise<FixHandlerResult>;
195
200
  export declare function handleJsonValuesExtractEntry(ctx: FixHandlerContext): Promise<FixHandlerResult>;
196
201
  /**
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A gallery entry's key, read as what it actually is.
3
+ *
4
+ * A gallery is a record keyed by its entries' file paths. A REMOTE gallery keys
5
+ * an uploaded entry by the remote URL instead — which encodes that same path —
6
+ * so every entry has a local path, and the interesting question is whether the
7
+ * bytes are expected to be at it.
8
+ *
9
+ * **They are not, for a remote entry added the ordinary way.** `saveOrUploadFiles`
10
+ * uploads the remote descriptors to the content host and copies only the local
11
+ * ones into the working tree, so an image added through the Studio (or over MCP)
12
+ * and published has no file in the repo, by design — putting one there is what
13
+ * remote storage exists to avoid. A remote entry CAN have one, because
14
+ * `val validate --fix` promotes a local file to a remote ref and leaves the file
15
+ * where it was; so "remote" means "do not require it", never "there is not one".
16
+ *
17
+ * One copy, in its own file, because this used to be two: a `remoteKeyToLocalPath`
18
+ * in `fixHandlers` and a `galleryKeyToLocalPath` in `createFixPatch`, each
19
+ * normalising the key correctly and each then treating the result as a file that
20
+ * must exist. That agreement is what made a published remote image report as
21
+ * missing from both sides at once.
22
+ */
23
+ export type GalleryEntryKey = {
24
+ /**
25
+ * Where the bytes are, or would be, in the working tree.
26
+ *
27
+ * The key itself for a remote key that does not parse — there is no such
28
+ * place for one, and nothing reads this for a remote entry except the
29
+ * untracked-files comparison, where a URL matches no file.
30
+ */
31
+ localPath: string;
32
+ /** True when the key is a URL, so a local file is not required. */
33
+ remote: boolean;
34
+ };
35
+ export declare function galleryEntryOf(key: string): GalleryEntryKey;
@@ -0,0 +1,59 @@
1
+ import { type GalleryEntryKey } from "./galleryEntryKey.js";
2
+ import type { IValFSHost } from "./ValFSHost.js";
3
+ /**
4
+ * What is out of step between a gallery's entries and its directory.
5
+ *
6
+ * Two questions, and they treat a remote entry differently — which is the whole
7
+ * reason this is separate from the handler around it:
8
+ *
9
+ * - **Missing**: an entry with no bytes at its local path. Asked of LOCAL
10
+ * entries only. A remote entry's bytes live on the content host, and nothing
11
+ * puts a copy in the working tree: `saveOrUploadFiles` uploads the remote
12
+ * descriptors and copies only the local ones into the tree, so a remote entry
13
+ * added through the Studio (or over MCP) has no local file by design, and
14
+ * demanding one would mean committing remote bytes to git — which is what
15
+ * remote storage exists to avoid. Whether those bytes really are on the host
16
+ * is a different check, `image:check-remote`, which already runs for exactly
17
+ * these entries.
18
+ * - **Untracked**: a file in the directory that no entry claims. Asked of every
19
+ * entry, remote included, and that is why they are normalised to their local
20
+ * path: `val validate --fix` promotes a local file to a remote ref and leaves
21
+ * the file where it was, so a remote entry can perfectly well have one.
22
+ *
23
+ * An entry can claim more than its key. A video set's HLS stream is keyed by
24
+ * its master playlist, and the media playlists and segments it names are the
25
+ * same entry — so `filesOfEntry` says what else an entry holds, and those are
26
+ * not untracked either.
27
+ *
28
+ * Its own module rather than a part of `fixHandlers.ts`, so the video set's
29
+ * fixes can use it without importing the handler registry they are part of.
30
+ */
31
+ export declare function checkGalleryFiles(input: {
32
+ entryKeys: string[];
33
+ dir: string;
34
+ projectRoot: string;
35
+ fs: Pick<IValFSHost, "fileExists" | "readDirectory">;
36
+ /**
37
+ * Every file an entry holds besides its key, as `/public/…` refs. Asked of
38
+ * every entry, for the same reason untracked is.
39
+ */
40
+ filesOfEntry?: (entry: GalleryEntryKey, key: string) => string[];
41
+ }): {
42
+ missingTrackedFiles: string[];
43
+ untrackedFiles: string[];
44
+ };
45
+ /**
46
+ * The LOCAL entries whose key is there but some of what they hold is not: a
47
+ * stream missing a segment plays up to the gap and then stops. Separate from
48
+ * `checkGalleryFiles`' "missing" because a fix does not remove these — the
49
+ * entry still names a video someone uploaded and may have the rest of.
50
+ */
51
+ export declare function incompleteGalleryEntries(input: {
52
+ entryKeys: string[];
53
+ projectRoot: string;
54
+ fs: Pick<IValFSHost, "fileExists">;
55
+ filesOfEntry: (entry: GalleryEntryKey, key: string) => string[];
56
+ }): {
57
+ key: string;
58
+ missing: string[];
59
+ }[];
@@ -4,6 +4,7 @@ export type { AdapterFor, BoundExternalRecord, ExternalAuthor, ExternalBuilder,
4
4
  export { createValApiRouter, createValServer, safeReadGit } from "./ValRouter.js";
5
5
  export { initHandlerOptions, createValOps } from "./valServerConfig.js";
6
6
  export { resolveRemoteFileAuth } from "./valServerConfig.js";
7
+ export { APP_MODE_REQUIRES_REMOTE_FILES } from "./valServerConfig.js";
7
8
  export type { RemoteFileAuth, ResolveRemoteFileAuthResult, } from "./valServerConfig.js";
8
9
  export { ValModuleLoader } from "./ValModuleLoader.js";
9
10
  export { getCompilerOptions } from "./getCompilerOptions.js";
@@ -17,13 +18,15 @@ export { analyzeValModule } from "./patch/ts/valModule.js";
17
18
  export type { ValModuleAnalysis } from "./patch/ts/valModule.js";
18
19
  export { createFixPatch } from "./createFixPatch.js";
19
20
  export { fixHandlers, currentFixHandlers, createDefaultValFSHost, handleFileMetadata, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleRemoteFileDownload, handleRemoteFileCheck, handleUniqueFolderCheck, handleCheckAllFiles, handleJsonValuesExtractEntry, handleExternalUpload, } from "./fixHandlers.js";
20
- export type { FixHandler, FixHandlerContext, FixHandlerResult, IValRemote, ValidationEvent, ValidationError, ValModule, } from "./fixHandlers.js";
21
+ export { handleVideosetMetadata, handleVideosetUploadRemote, handleVideosetCheckRemote, handleVideosetCheckAllFiles, videosetEntryVideoSchema, filesOfVideosetEntry, } from "./videosetFixes.js";
22
+ export type { FixHandler, ModulePatch, FixHandlerContext, FixHandlerResult, IValRemote, ValidationEvent, ValidationError, ValModule, } from "./fixHandlers.js";
21
23
  export * from "./jwt.js";
22
24
  export type { ValServer } from "./ValServer.js";
23
25
  export { getSettings } from "./getSettings.js";
24
26
  export { getPersonalAccessTokenPath, parsePersonalAccessTokenFile, } from "./personalAccessTokens.js";
25
27
  export { uploadRemoteFile } from "./uploadRemoteFile.js";
26
- export { extractImageMetadata, extractFileMetadata } from "./extractMetadata.js";
28
+ export { extractImageMetadata, extractFileMetadata, extractVideoMetadata, extractVideoMetadataFromFile, } from "./extractMetadata.js";
29
+ export { filesOfVideo } from "./videoFiles.js";
27
30
  export { validateMetadata } from "./validateMetadata.js";
28
31
  export { getValidationErrorFileRef } from "./getValidationErrorFileRef.js";
29
32
  export { checkRemoteRef, downloadFileFromRemote, getCachedRemoteFileDir, getCachedRemoteFilePath, } from "./checkRemoteRef.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The size and length of an ISO base media file (`.mp4`, `.m4v`, `.mov`),
3
+ * read from its headers. Nothing is decoded.
4
+ *
5
+ * Hand-written rather than a media library, deliberately: `@valbuild/server`
6
+ * is loaded by every app that runs Val, and the four numbers wanted here are
7
+ * three small boxes away from the start of the `moov` box:
8
+ *
9
+ * moov > mvhd timescale and duration (version 0 and 1)
10
+ * moov > mvex > mehd duration of a FRAGMENTED file, whose mvhd says 0
11
+ * moov > trak > tkhd width and height (16.16 fixed point) and the
12
+ * display matrix, for a rotated phone video
13
+ * moov > trak > mdia > hdlr `vide`: which trak is the picture
14
+ *
15
+ * Only the box headers are read on the way to `moov`, and then `moov` itself,
16
+ * so a multi-GB file costs a few small reads: `mdat` is seeked past, never
17
+ * loaded. That is what {@link ByteSource} is for.
18
+ */
19
+ /** Random access to a file's bytes, so a large `mdat` can be skipped. */
20
+ export type ByteSource = {
21
+ readonly size: number;
22
+ /** Up to `length` bytes from `offset`; fewer only at the end of the file. */
23
+ read(offset: number, length: number): Buffer;
24
+ };
25
+ export declare function bufferByteSource(buffer: Buffer): ByteSource;
26
+ /**
27
+ * Run `read` against a file on disk, reading only what it asks for. The file
28
+ * is closed afterwards whatever happens.
29
+ */
30
+ export declare function withFileByteSource<T>(absolutePath: string, read: (source: ByteSource) => T): T;
31
+ export type IsoBmffMetadata = {
32
+ width?: number;
33
+ height?: number;
34
+ /** Seconds, unrounded. */
35
+ duration?: number;
36
+ };
37
+ /**
38
+ * Whatever of {@link IsoBmffMetadata} the file declares. A field that cannot be
39
+ * read is absent; nothing here throws on a file that is not ISO BMFF.
40
+ */
41
+ export declare function readIsoBmffMetadata(source: ByteSource): IsoBmffMetadata;
@@ -24,6 +24,14 @@ import { ValOpsMemory } from "./ValOpsMemory.js";
24
24
  * exists to avoid.
25
25
  */
26
26
  export declare const DEFAULT_VAL_BUILD_URL = "https://admin.val.build";
27
+ /**
28
+ * What the Val app says when a project does not store its media remotely.
29
+ *
30
+ * Exported so that `val publish` -- which publishes to the app, and so is
31
+ * the last place to catch this before a build goes out -- refuses with the
32
+ * same sentence the deployed server would.
33
+ */
34
+ export declare const APP_MODE_REQUIRES_REMOTE_FILES: string;
27
35
  /**
28
36
  * Resolve options plus environment into a concrete {@link ValServerConfig}.
29
37
  *
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Every file a video value names, as refs (`/public/…`, or a remote URL as
3
+ * written): the video, its poster, each caption track — and, for a LOCAL HLS
4
+ * stream, every file its playlists name, followed from the master down through
5
+ * each media playlist to the segment files, init sections and audio tracks.
6
+ *
7
+ * Followed rather than "everything in the master's directory": the Studio
8
+ * gives each stream a directory of its own, but nothing stops a hand-placed
9
+ * `/public/val/clip.m3u8`, and the directory of THAT one is every file in
10
+ * `/public/val`. A file in a stream's directory that no playlist names is not
11
+ * used by the stream either, so calling it unused is right.
12
+ *
13
+ * A remote stream's playlists are on the content host, so its master is all
14
+ * that is named here.
15
+ *
16
+ * The one place that answers this, so `list-unused-files` and anything else
17
+ * asking "is this file used" agree on what a video holds.
18
+ */
19
+ export declare function filesOfVideo(video: unknown, options: {
20
+ projectRoot: string;
21
+ readFile?: (absolutePath: string) => Buffer | undefined;
22
+ }): string[];