@valbuild/shared 0.121.0 → 0.123.0

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,183 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.123.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#613](https://github.com/valbuild/val/pull/613) [`c5e7dfd`](https://github.com/valbuild/val/commit/c5e7dfd12aa1371195be642e1c2fb72b6f3e3ce2) Thanks [@freekh](https://github.com/freekh)! - Val's MCP endpoint moves to its own package, and can now upload images
8
+
9
+ Everything that serves Val's content tools over MCP — the tool registry, the
10
+ write path behind it, the request guards and the access-token verification —
11
+ now lives in **`@valbuild/mcp`** instead of being split between
12
+ `@valbuild/server` and `@valbuild/next`.
13
+
14
+ **Nothing changes for an app that already mounts it.** `initValMcp` is still
15
+ exported from `@valbuild/next/server` and behaves exactly as before; it is now a
16
+ ten-line binding over `@valbuild/mcp`, supplying the Next version that
17
+ `initHandlerOptions` asks for. A host that is not Next can call
18
+ `initValMcp` from `@valbuild/mcp` directly.
19
+
20
+ If you built your own host on `createValTools`, import it from `@valbuild/mcp`
21
+ rather than `@valbuild/server`; the tool types moved with it.
22
+
23
+ ## Image uploads
24
+
25
+ An agent can now add an image, with a new `upload_image` tool. It takes a path
26
+ to a file on the machine your app runs on, or the image inline as base64, and
27
+ puts it in an `s.image()` field or an `s.images()` gallery.
28
+
29
+ Uploads are converted **only where the Studio would convert them**: `encode` is
30
+ off unless the schema asks for it (`s.image({ encode: { type: "webp" } })`), and
31
+ when it does, which images are converted, how far they are scaled and when the
32
+ original wins are the same decisions the browser makes — the same code, now
33
+ shared. An upload to a schema without `encode` is stored exactly as it arrived,
34
+ whatever its size.
35
+
36
+ One thing the tool does that the Studio does not: it refuses an image the
37
+ schema's `accept` does not cover, checked on the bytes that would actually be
38
+ stored. The Studio does not need to — its file picker carries `accept` — and
39
+ validation reports a mismatch as server-repairable, so nothing downstream would
40
+ stop it. An agent has no picker. Note the ordering:
41
+ `s.image({ accept: "image/webp", encode: { type: "webp" } })` still takes a PNG,
42
+ because the conversion happens first and it is the result that is checked.
43
+
44
+ It is the one tool you construct yourself, because it needs an image library
45
+ and `sharp` ships a compiled binary per platform. Val does not put one in every
46
+ project that installs it, so you decide:
47
+
48
+ ```sh
49
+ npm install sharp
50
+ ```
51
+
52
+ ```ts
53
+ import sharp from "sharp";
54
+ import { createValImageTools } from "@valbuild/mcp";
55
+ import { sharpImageProcessor } from "@valbuild/mcp/sharp";
56
+
57
+ const { valMcpAuthorize, valMcpTools } = initValMcp(valModules, config, {
58
+ extraTools: createValImageTools(sharpImageProcessor(sharp)),
59
+ });
60
+ ```
61
+
62
+ Leave `extraTools` out and everything else works as before — the agent can read,
63
+ validate and edit content, it just cannot add an image. `sharp` is passed in
64
+ rather than imported, so you can supply another encoder: `ValImageProcessor` is
65
+ two functions, `read` and `encode`.
66
+
67
+ Remotely stored images work too — `s.image().remote()` and
68
+ `s.images({ remote: true })` — and they need nothing extra from the MCP client.
69
+ Adding one does not upload anything to Val's content host: the bytes go into the
70
+ patch store like any other unpublished change, and the push to
71
+ `remote.val.build` happens when you publish, exactly as it does for an image
72
+ added through the Studio. All the tool has to do first is ask the project which
73
+ bucket to name in the ref, and the credential for that is the one your app
74
+ already has — its API key when it has one, and in local development the
75
+ `val login` token in your project, the same one `val validate --fix` uses. If
76
+ you have not logged in, it says so and writes nothing.
77
+
78
+ ## `npm create @valbuild` asks
79
+
80
+ The starter template now ships the MCP endpoint, and `npm create @valbuild`
81
+ asks whether you want it — and, if you do, whether agents should be able to
82
+ upload images, saying that this adds `sharp`. Both default to yes, and both can
83
+ be answered up front for a scripted setup:
84
+
85
+ ```sh
86
+ pnpm create @valbuild my-app --mcp --no-image-uploads
87
+ ```
88
+
89
+ - [#620](https://github.com/valbuild/val/pull/620) [`53f670c`](https://github.com/valbuild/val/commit/53f670c0cf2d7a03a6d068c78b7874ce77652c2a) Thanks [@freekh](https://github.com/freekh)! - MCP: `search_content` — full-text search across the project's content, unpublished changes included.
90
+
91
+ The same index the Studio's search box uses, moved into `@valbuild/shared` so both run it, and built fresh on every call. That was the part that looked expensive and is not: indexing `valbuild/web`, a real production site of 20 modules and 206 KB of source, takes 162 ms, and it scales linearly from there — the 10 s default deadline is not reached until roughly 13 MB of content. Loading the modules costs more than indexing them, and every tool call already pays that.
92
+
93
+ That ratio between building the index and querying it — 162 ms against 0.2 ms — is why `queries` is a list. Everything expensive happens before the first query runs, so up to 20 of them are answered from a single pass, separately, so a caller can tell which of its guesses found the thing. A bare string still works as one query.
94
+
95
+ It returns source paths, so `get_source` reads what it finds. The rest of the arguments narrow or bound the work:
96
+
97
+ - `include` / `exclude` — module file path globs, e.g. `["/content/blogs/**"]`. `exclude` is applied after `include`.
98
+ - `limit` — per query, defaulting to 100. A model filters a long list more cheaply than it asks again.
99
+ - `timeoutMs` — stop indexing and answer with what has been indexed so far. The result then carries `timedOut`, the paths of the modules that were not reached, and a hint pointing at `include`.
100
+
101
+ Every answer says what it actually searched (`searched: { modules, of }`), so each query's `total` can be read as a count over those modules rather than over the project. Modules you excluded are not reported as omissions — an omission always means the deadline, never your filter.
102
+
103
+ `performSearch` now counts all the matches rather than the page it returns, which the Studio's result count gets too: FlexSearch stops as soon as it has the ids it was asked for, so a total taken from a page-sized search was only ever the page size again.
104
+
105
+ ## 0.122.0
106
+
107
+ ### Minor Changes
108
+
109
+ - [#563](https://github.com/valbuild/val/pull/563) [`be32261`](https://github.com/valbuild/val/commit/be32261af19db8018bc37b180d903416018c0b79) Thanks [@freekh](https://github.com/freekh)! - See how a module looked at any past commit, and restore from it by pointing at it
110
+
111
+ Val could publish edits but never look back. Now every commit is a durable
112
+ record you can open, and any part of it can be put back.
113
+
114
+ **Open a commit and the Studio splits in two.** The left half is the Studio
115
+ itself — same navigation, same fields, same everything, because it _is_ the
116
+ editor rather than a copy of it. The right half shows the project as that commit
117
+ left it. On a phone the two become one pane and a toggle.
118
+
119
+ **Restoring is directed: you point at the old value, then at where it goes.**
120
+ Val does not try to work out which of today's fields corresponds to which of the
121
+ commit's. It cannot be sure — array items splice, schemas move — and a restore
122
+ that guesses wrong writes into the wrong place and looks like it worked. Two
123
+ picks leave nothing to guess. You can restore across paths, so last month's
124
+ headline can become today's tagline.
125
+
126
+ Before you click, every field on the "now" side says whether it can hold the
127
+ value you picked, and a field that cannot explains why when you click it rather
128
+ than doing nothing. A changed union is not itself a blocker: what matters is
129
+ whether the value's own shape is still allowed, so a union that gained a case
130
+ restores fine and one that lost the case you are restoring does not.
131
+
132
+ Rich text can be restored but is marked "probably fits" rather than confirmed —
133
+ comparing every mark and block against the options a schema allows is not done
134
+ yet, and saying so is better than a confident answer we cannot back. It is
135
+ checked properly the moment you commit to it: before anything is staged, the old
136
+ value is checked against the field it is going into, and a value that cannot be
137
+ that field is refused with the reason. A value that is the right shape but
138
+ breaks a rule about its content — a name too short for its `minLength` — is
139
+ staged and then held at publish, the same as if you had typed it, because a
140
+ restore should not be stricter than typing.
141
+
142
+ **A whole module can be put back on its own**, from a commit that changed
143
+ several, without reverting the rest of the commit.
144
+
145
+ **Restores are staged, not applied.** They land in pending changes, are reviewed
146
+ beside every other edit, and go out with the next publish. There is also "put
147
+ everything back", for when a whole publish was the mistake.
148
+
149
+ To make this possible, publishing now records each changed module's data and the
150
+ schema it was written against. Not the `.val.ts` — git already keeps that, but
151
+ it is code, and turning code back into data means parsing it, which is
152
+ best-effort and stops working as TypeScript, your runtime and Val move on. The
153
+ schema is kept because a value on its own cannot be drawn: showing a module as it
154
+ was at a commit whose schema has since changed needs _that commit's_ schema, and
155
+ nothing in your current checkout has it.
156
+
157
+ Things it will not pretend about: a module the commit did not touch says so
158
+ rather than showing today's value; a module saved by a different version of Val
159
+ says the version differs and that nothing is lost; a commit made before Val
160
+ started recording history disables restore with the reason next to it. Images
161
+ and files are restored by re-uploading them, since the bytes at an old commit
162
+ may no longer be on your branch.
163
+
164
+ History requires the Val content service. In filesystem mode it reports
165
+ `not-supported-in-fs-mode` rather than faking it from git, which has the files
166
+ but not which of a commit's changes were one editor's work.
167
+
168
+ ### Patch Changes
169
+
170
+ - [#618](https://github.com/valbuild/val/pull/618) [`da6794f`](https://github.com/valbuild/val/commit/da6794f3dbd77d49ccfe780b359bab1689ee1b11) Thanks [@freekh](https://github.com/freekh)! - Remove the unused `GET /api/val/session` endpoint.
171
+
172
+ Nothing called it. The Studio reads the profile id from `/stat`, and in proxy
173
+ mode the route proxied to `${VAL_BUILD_URL}/api/val/${project}/auth/session`,
174
+ an upstream route that no longer exists — so calling it by hand returned a 500
175
+ rather than a session. It is gone from both the route declarations in
176
+ `@valbuild/shared` and the implementation in `@valbuild/server`.
177
+
178
+ Session cookie handling itself is unchanged: `/authorize`, `/callback` and
179
+ `/logout` still set and clear `val_session` as before.
180
+
3
181
  ## 0.121.0
4
182
 
5
183
  ### Minor Changes
@@ -273,46 +273,6 @@ export declare const Api: {
273
273
  }, z.core.$strip>;
274
274
  };
275
275
  };
276
- "/session": {
277
- GET: {
278
- req: {
279
- cookies: {
280
- val_session: z.ZodOptional<z.ZodString>;
281
- };
282
- };
283
- res: z.ZodUnion<readonly [z.ZodObject<{
284
- status: z.ZodLiteral<200>;
285
- json: z.ZodObject<{
286
- mode: z.ZodUnion<readonly [z.ZodLiteral<"local">, z.ZodLiteral<"proxy">]>;
287
- enabled: z.ZodBoolean;
288
- }, z.core.$strip>;
289
- }, z.core.$strip>, z.ZodObject<{
290
- status: z.ZodUnion<readonly [z.ZodLiteral<400>, z.ZodLiteral<401>, z.ZodLiteral<403>, z.ZodLiteral<404>, z.ZodLiteral<500>, z.ZodLiteral<501>]>;
291
- json: z.ZodObject<{
292
- message: z.ZodString;
293
- }, z.core.$strip>;
294
- }, z.core.$strip>, z.ZodObject<{
295
- status: z.ZodLiteral<401>;
296
- json: z.ZodObject<{
297
- message: z.ZodString;
298
- details: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
299
- reason: z.ZodString;
300
- }, z.core.$strip>, z.ZodObject<{
301
- sub: z.ZodString;
302
- exp: z.ZodNumber;
303
- token: z.ZodString;
304
- org: z.ZodString;
305
- project: z.ZodString;
306
- }, z.core.$strip>]>;
307
- }, z.core.$strip>;
308
- }, z.core.$strip>, z.ZodObject<{
309
- status: z.ZodLiteral<500>;
310
- json: z.ZodObject<{
311
- message: z.ZodString;
312
- }, z.core.$strip>;
313
- }, z.core.$strip>]>;
314
- };
315
- };
316
276
  "/logout": {
317
277
  GET: {
318
278
  req: {
@@ -980,7 +940,7 @@ export declare const Api: {
980
940
  message: z.ZodString;
981
941
  value: z.ZodOptional<z.ZodUnknown>;
982
942
  fatal: z.ZodOptional<z.ZodBoolean>;
983
- fixes: z.ZodOptional<z.ZodArray<z.ZodType<"image:add-metadata" | "image:check-metadata" | "image:upload-remote" | "image:download-remote" | "image:check-remote" | "images:check-remote" | "images:upload-remote" | "file:add-metadata" | "file:check-metadata" | "file:upload-remote" | "file:download-remote" | "file:check-remote" | "files:check-remote" | "files:upload-remote" | "keyof:check-keys" | "router:check-route" | "images:check-unique-folder" | "files:check-unique-folder" | "images:check-all-files" | "files:check-all-files" | "jsonValues:extract-entry", unknown, z.core.$ZodTypeInternals<"image:add-metadata" | "image:check-metadata" | "image:upload-remote" | "image:download-remote" | "image:check-remote" | "images:check-remote" | "images:upload-remote" | "file:add-metadata" | "file:check-metadata" | "file:upload-remote" | "file:download-remote" | "file:check-remote" | "files:check-remote" | "files:upload-remote" | "keyof:check-keys" | "router:check-route" | "images:check-unique-folder" | "files:check-unique-folder" | "images:check-all-files" | "files:check-all-files" | "jsonValues:extract-entry", unknown>>>>;
943
+ fixes: z.ZodOptional<z.ZodArray<z.ZodType<"image:add-metadata" | "image:check-metadata" | "image:upload-remote" | "image:download-remote" | "image:check-remote" | "images:check-remote" | "images:upload-remote" | "file:add-metadata" | "file:check-metadata" | "file:upload-remote" | "file:download-remote" | "file:check-remote" | "files:check-remote" | "files:upload-remote" | "keyof:check-keys" | "router:check-route" | "images:check-unique-folder" | "files:check-unique-folder" | "images:check-all-files" | "files:check-all-files" | "jsonValues:extract-entry" | "external:upload", unknown, z.core.$ZodTypeInternals<"image:add-metadata" | "image:check-metadata" | "image:upload-remote" | "image:download-remote" | "image:check-remote" | "images:check-remote" | "images:upload-remote" | "file:add-metadata" | "file:check-metadata" | "file:upload-remote" | "file:download-remote" | "file:check-remote" | "files:check-remote" | "files:upload-remote" | "keyof:check-keys" | "router:check-route" | "images:check-unique-folder" | "files:check-unique-folder" | "images:check-all-files" | "files:check-all-files" | "jsonValues:extract-entry" | "external:upload", unknown>>>>;
984
944
  }, z.core.$strip>>>>;
985
945
  }, z.core.$strip>>;
986
946
  }, z.core.$strip>;
@@ -1096,6 +1056,309 @@ export declare const Api: {
1096
1056
  }, z.core.$strip>]>;
1097
1057
  };
1098
1058
  };
1059
+ "/history/commits": {
1060
+ GET: {
1061
+ req: {
1062
+ query: {
1063
+ branch: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
1064
+ limit: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>, z.ZodTransform<number, string>>>;
1065
+ /** The previous page's `nextCursor`. */
1066
+ cursor: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>>;
1067
+ };
1068
+ cookies: {
1069
+ val_session: z.ZodOptional<z.ZodString>;
1070
+ };
1071
+ };
1072
+ res: z.ZodUnion<readonly [z.ZodObject<{
1073
+ status: z.ZodLiteral<401>;
1074
+ json: z.ZodObject<{
1075
+ message: z.ZodString;
1076
+ }, z.core.$strip>;
1077
+ }, z.core.$strip>, z.ZodObject<{
1078
+ status: z.ZodLiteral<404>;
1079
+ json: z.ZodObject<{
1080
+ message: z.ZodString;
1081
+ }, z.core.$strip>;
1082
+ }, z.core.$strip>, z.ZodObject<{
1083
+ status: z.ZodLiteral<400>;
1084
+ json: z.ZodIntersection<z.ZodObject<{
1085
+ message: z.ZodString;
1086
+ }, z.core.$strip>, z.ZodObject<{
1087
+ kind: z.ZodOptional<z.ZodString>;
1088
+ }, z.core.$strip>>;
1089
+ }, z.core.$strip>, z.ZodObject<{
1090
+ status: z.ZodLiteral<500>;
1091
+ json: z.ZodIntersection<z.ZodObject<{
1092
+ message: z.ZodString;
1093
+ }, z.core.$strip>, z.ZodObject<{
1094
+ kind: z.ZodOptional<z.ZodString>;
1095
+ }, z.core.$strip>>;
1096
+ }, z.core.$strip>, z.ZodObject<{
1097
+ status: z.ZodLiteral<200>;
1098
+ json: z.ZodObject<{
1099
+ commits: z.ZodArray<z.ZodObject<{
1100
+ commitSha: z.ZodString;
1101
+ parentCommitSha: z.ZodString;
1102
+ clientCommitSha: z.ZodString;
1103
+ branch: z.ZodString;
1104
+ createdBranch: z.ZodNullable<z.ZodString>;
1105
+ creator: z.ZodNullable<z.ZodString>;
1106
+ message: z.ZodNullable<z.ZodString>;
1107
+ createdAt: z.ZodString;
1108
+ seqNum: z.ZodString;
1109
+ patchCount: z.ZodNumber;
1110
+ hasArchive: z.ZodBoolean;
1111
+ }, z.core.$strip>>;
1112
+ nextCursor: z.ZodNullable<z.ZodString>;
1113
+ }, z.core.$strip>;
1114
+ }, z.core.$strip>]>;
1115
+ };
1116
+ };
1117
+ /**
1118
+ * One commit, reconstructed. IMMUTABLE for a given sha - it says nothing
1119
+ * about the current source or schema, which is what lets it be cached
1120
+ * forever and what makes comparing many commits cheap.
1121
+ */
1122
+ "/history/commit": {
1123
+ GET: {
1124
+ req: {
1125
+ query: {
1126
+ commit_sha: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
1127
+ };
1128
+ cookies: {
1129
+ val_session: z.ZodOptional<z.ZodString>;
1130
+ };
1131
+ };
1132
+ res: z.ZodUnion<readonly [z.ZodObject<{
1133
+ status: z.ZodLiteral<401>;
1134
+ json: z.ZodObject<{
1135
+ message: z.ZodString;
1136
+ }, z.core.$strip>;
1137
+ }, z.core.$strip>, z.ZodObject<{
1138
+ status: z.ZodLiteral<404>;
1139
+ json: z.ZodObject<{
1140
+ message: z.ZodString;
1141
+ }, z.core.$strip>;
1142
+ }, z.core.$strip>, z.ZodObject<{
1143
+ status: z.ZodLiteral<400>;
1144
+ json: z.ZodObject<{
1145
+ message: z.ZodString;
1146
+ }, z.core.$strip>;
1147
+ }, z.core.$strip>, z.ZodObject<{
1148
+ status: z.ZodLiteral<500>;
1149
+ json: z.ZodIntersection<z.ZodObject<{
1150
+ message: z.ZodString;
1151
+ }, z.core.$strip>, z.ZodObject<{
1152
+ kind: z.ZodOptional<z.ZodString>;
1153
+ }, z.core.$strip>>;
1154
+ }, z.core.$strip>, z.ZodObject<{
1155
+ status: z.ZodLiteral<200>;
1156
+ json: z.ZodObject<{
1157
+ commit: z.ZodObject<{
1158
+ commitSha: z.ZodString;
1159
+ parentCommitSha: z.ZodString;
1160
+ clientCommitSha: z.ZodString;
1161
+ branch: z.ZodString;
1162
+ createdBranch: z.ZodNullable<z.ZodString>;
1163
+ creator: z.ZodNullable<z.ZodString>;
1164
+ message: z.ZodNullable<z.ZodString>;
1165
+ createdAt: z.ZodString;
1166
+ seqNum: z.ZodString;
1167
+ patchCount: z.ZodNumber;
1168
+ hasArchive: z.ZodBoolean;
1169
+ }, z.core.$strip>;
1170
+ modules: z.ZodRecord<z.ZodString, z.ZodObject<{
1171
+ source: z.ZodNullable<z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>>;
1172
+ schema: z.ZodNullable<z.ZodType<import("@valbuild/core").SerializedSchema, unknown, z.core.$ZodTypeInternals<import("@valbuild/core").SerializedSchema, unknown>>>;
1173
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
1174
+ changedPaths: z.ZodArray<z.ZodString>;
1175
+ failures: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
1176
+ kind: z.ZodLiteral<"commit-not-found">;
1177
+ commitSha: z.ZodString;
1178
+ }, z.core.$strip>, z.ZodObject<{
1179
+ kind: z.ZodLiteral<"archive-unreadable">;
1180
+ commitSha: z.ZodString;
1181
+ message: z.ZodString;
1182
+ }, z.core.$strip>, z.ZodObject<{
1183
+ kind: z.ZodLiteral<"source-unavailable">;
1184
+ moduleFilePath: z.ZodString;
1185
+ }, z.core.$strip>, z.ZodObject<{
1186
+ kind: z.ZodLiteral<"schema-unreadable">;
1187
+ moduleFilePath: z.ZodString;
1188
+ message: z.ZodString;
1189
+ }, z.core.$strip>, z.ZodObject<{
1190
+ kind: z.ZodLiteral<"file-unavailable">;
1191
+ gitPath: z.ZodString;
1192
+ message: z.ZodString;
1193
+ }, z.core.$strip>, z.ZodObject<{
1194
+ kind: z.ZodLiteral<"not-supported-in-fs-mode">;
1195
+ }, z.core.$strip>, z.ZodObject<{
1196
+ kind: z.ZodLiteral<"transport">;
1197
+ message: z.ZodString;
1198
+ }, z.core.$strip>], "kind">>;
1199
+ }, z.core.$strip>>;
1200
+ patches: z.ZodArray<z.ZodObject<{
1201
+ patchId: z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>;
1202
+ moduleFilePath: z.ZodString;
1203
+ patch: z.ZodUnknown;
1204
+ authorId: z.ZodNullable<z.ZodString>;
1205
+ createdAt: z.ZodString;
1206
+ baseSha: z.ZodString;
1207
+ coreVersion: z.ZodString;
1208
+ }, z.core.$strip>>;
1209
+ jsonEntries: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1210
+ binaryFiles: z.ZodArray<z.ZodObject<{
1211
+ gitPath: z.ZodString;
1212
+ change: z.ZodUnion<readonly [z.ZodLiteral<"added">, z.ZodLiteral<"modified">, z.ZodLiteral<"deleted">]>;
1213
+ remote: z.ZodBoolean;
1214
+ url: z.ZodString;
1215
+ }, z.core.$strip>>;
1216
+ warnings: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
1217
+ kind: z.ZodLiteral<"commit-not-found">;
1218
+ commitSha: z.ZodString;
1219
+ }, z.core.$strip>, z.ZodObject<{
1220
+ kind: z.ZodLiteral<"archive-unreadable">;
1221
+ commitSha: z.ZodString;
1222
+ message: z.ZodString;
1223
+ }, z.core.$strip>, z.ZodObject<{
1224
+ kind: z.ZodLiteral<"source-unavailable">;
1225
+ moduleFilePath: z.ZodString;
1226
+ }, z.core.$strip>, z.ZodObject<{
1227
+ kind: z.ZodLiteral<"schema-unreadable">;
1228
+ moduleFilePath: z.ZodString;
1229
+ message: z.ZodString;
1230
+ }, z.core.$strip>, z.ZodObject<{
1231
+ kind: z.ZodLiteral<"file-unavailable">;
1232
+ gitPath: z.ZodString;
1233
+ message: z.ZodString;
1234
+ }, z.core.$strip>, z.ZodObject<{
1235
+ kind: z.ZodLiteral<"not-supported-in-fs-mode">;
1236
+ }, z.core.$strip>, z.ZodObject<{
1237
+ kind: z.ZodLiteral<"transport">;
1238
+ message: z.ZodString;
1239
+ }, z.core.$strip>], "kind">>;
1240
+ }, z.core.$strip>;
1241
+ }, z.core.$strip>]>;
1242
+ };
1243
+ };
1244
+ /**
1245
+ * ONE module, as of a commit — including a commit that did not change it.
1246
+ *
1247
+ * `/history/commit` covers what a commit changed, which is the narrow case:
1248
+ * navigate the history pane to anything else and there would be nothing to
1249
+ * show. This answers "how did this module look at that point", which is the
1250
+ * question someone comparing two panes is actually asking. Immutable for the
1251
+ * same reason: what a commit left behind cannot change.
1252
+ */
1253
+ "/history/module": {
1254
+ GET: {
1255
+ req: {
1256
+ query: {
1257
+ commit_sha: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
1258
+ module_file_path: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
1259
+ };
1260
+ cookies: {
1261
+ val_session: z.ZodOptional<z.ZodString>;
1262
+ };
1263
+ };
1264
+ res: z.ZodUnion<readonly [z.ZodObject<{
1265
+ status: z.ZodLiteral<401>;
1266
+ json: z.ZodObject<{
1267
+ message: z.ZodString;
1268
+ }, z.core.$strip>;
1269
+ }, z.core.$strip>, z.ZodObject<{
1270
+ status: z.ZodLiteral<404>;
1271
+ json: z.ZodObject<{
1272
+ message: z.ZodString;
1273
+ }, z.core.$strip>;
1274
+ }, z.core.$strip>, z.ZodObject<{
1275
+ status: z.ZodLiteral<400>;
1276
+ json: z.ZodObject<{
1277
+ message: z.ZodString;
1278
+ }, z.core.$strip>;
1279
+ }, z.core.$strip>, z.ZodObject<{
1280
+ status: z.ZodLiteral<500>;
1281
+ json: z.ZodIntersection<z.ZodObject<{
1282
+ message: z.ZodString;
1283
+ }, z.core.$strip>, z.ZodObject<{
1284
+ kind: z.ZodOptional<z.ZodString>;
1285
+ }, z.core.$strip>>;
1286
+ }, z.core.$strip>, z.ZodObject<{
1287
+ status: z.ZodLiteral<200>;
1288
+ json: z.ZodObject<{
1289
+ moduleFilePath: z.ZodString;
1290
+ module: z.ZodNullable<z.ZodObject<{
1291
+ source: z.ZodNullable<z.ZodType<import("@valbuild/core/patch").JSONValue, unknown, z.core.$ZodTypeInternals<import("@valbuild/core/patch").JSONValue, unknown>>>;
1292
+ schema: z.ZodNullable<z.ZodType<import("@valbuild/core").SerializedSchema, unknown, z.core.$ZodTypeInternals<import("@valbuild/core").SerializedSchema, unknown>>>;
1293
+ patchIds: z.ZodArray<z.ZodString & z.ZodType<import("@valbuild/core").PatchId, string, z.core.$ZodTypeInternals<import("@valbuild/core").PatchId, string>>>;
1294
+ changedPaths: z.ZodArray<z.ZodString>;
1295
+ failures: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
1296
+ kind: z.ZodLiteral<"commit-not-found">;
1297
+ commitSha: z.ZodString;
1298
+ }, z.core.$strip>, z.ZodObject<{
1299
+ kind: z.ZodLiteral<"archive-unreadable">;
1300
+ commitSha: z.ZodString;
1301
+ message: z.ZodString;
1302
+ }, z.core.$strip>, z.ZodObject<{
1303
+ kind: z.ZodLiteral<"source-unavailable">;
1304
+ moduleFilePath: z.ZodString;
1305
+ }, z.core.$strip>, z.ZodObject<{
1306
+ kind: z.ZodLiteral<"schema-unreadable">;
1307
+ moduleFilePath: z.ZodString;
1308
+ message: z.ZodString;
1309
+ }, z.core.$strip>, z.ZodObject<{
1310
+ kind: z.ZodLiteral<"file-unavailable">;
1311
+ gitPath: z.ZodString;
1312
+ message: z.ZodString;
1313
+ }, z.core.$strip>, z.ZodObject<{
1314
+ kind: z.ZodLiteral<"not-supported-in-fs-mode">;
1315
+ }, z.core.$strip>, z.ZodObject<{
1316
+ kind: z.ZodLiteral<"transport">;
1317
+ message: z.ZodString;
1318
+ }, z.core.$strip>], "kind">>;
1319
+ }, z.core.$strip>>;
1320
+ }, z.core.$strip>;
1321
+ }, z.core.$strip>]>;
1322
+ };
1323
+ };
1324
+ /**
1325
+ * One file as it was at one commit.
1326
+ *
1327
+ * The lazy half: `/history/commit` NAMES a commit's binary files and this is
1328
+ * where their bytes come from, so nothing is fetched until an `<img>`
1329
+ * actually mounts. A file at a fixed commit cannot change, so the response is
1330
+ * immutable and the browser's own cache handles flipping between commits.
1331
+ */
1332
+ "/history/files": {
1333
+ GET: {
1334
+ req: {
1335
+ query: {
1336
+ commit_sha: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
1337
+ path: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>;
1338
+ remote: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string, string[]>>>;
1339
+ };
1340
+ };
1341
+ res: z.ZodUnion<readonly [z.ZodObject<{
1342
+ status: z.ZodLiteral<401>;
1343
+ json: z.ZodObject<{
1344
+ message: z.ZodString;
1345
+ }, z.core.$strip>;
1346
+ }, z.core.$strip>, z.ZodObject<{
1347
+ status: z.ZodLiteral<404>;
1348
+ json: z.ZodObject<{
1349
+ message: z.ZodString;
1350
+ }, z.core.$strip>;
1351
+ }, z.core.$strip>, z.ZodObject<{
1352
+ status: z.ZodLiteral<400>;
1353
+ json: z.ZodObject<{
1354
+ message: z.ZodString;
1355
+ }, z.core.$strip>;
1356
+ }, z.core.$strip>, z.ZodObject<{
1357
+ status: z.ZodLiteral<200>;
1358
+ body: z.ZodCustom<import("node:stream/web").ReadableStream<unknown>, import("node:stream/web").ReadableStream<unknown>>;
1359
+ }, z.core.$strip>]>;
1360
+ };
1361
+ };
1099
1362
  "/files": {
1100
1363
  GET: {
1101
1364
  req: {
@@ -3,11 +3,13 @@ export * from "./server/types.js";
3
3
  export * from "./ValClient.js";
4
4
  export * from "./ValUrls.js";
5
5
  export * from "./ApiRoutes.js";
6
+ export * from "./schema/compatibility.js";
6
7
  export * from "./newestCommitSha.js";
7
8
  export * from "./zod/Patch.js";
8
9
  export * from "./sessionStorage.js";
9
10
  export * from "./SharedValConfig.js";
10
11
  export * from "./zod/ValCommit.js";
12
+ export * from "./zod/History.js";
11
13
  export * from "./zod/ValDeployment.js";
12
14
  export * from "./getSitemapTree.js";
13
15
  export * from "./parseRoutePattern.js";
@@ -23,3 +25,8 @@ export * from "./aiTools/aiImageToolPatches.js";
23
25
  export * from "./aiTools/aiSourceToolPatches.js";
24
26
  export * from "./validation/partitionValidationErrors.js";
25
27
  export * from "./validation/blockingValidationErrors.js";
28
+ export * from "./media/encodeImageDecisions.js";
29
+ export * from "./search/searchIndex.js";
30
+ export * from "./search/traverseSchemaSource.js";
31
+ export * from "./search/getFilenameFromRef.js";
32
+ export * from "./search/sourcePath.js";
@@ -0,0 +1,89 @@
1
+ import { type ImageEncodeOption } from "@valbuild/core";
2
+ /**
3
+ * Every decision `encode` makes, with none of the pixels.
4
+ *
5
+ * The re-encode itself happens in two places that share nothing: the Studio
6
+ * runs it on a `<canvas>` before the upload leaves the browser
7
+ * (`packages/ui/spa/utils/encodeImage.ts`), and the MCP image tool runs it on
8
+ * `sharp` in the server process (`@valbuild/mcp`). What must NOT differ between
9
+ * them is what `s.image({ encode })` means — which images are converted, how
10
+ * far they are scaled, and when the original wins anyway. So the decisions live
11
+ * here, once, and each side supplies only its own encoder.
12
+ *
13
+ * See `architecture/media.md`.
14
+ */
15
+ export type EncodeSettings = {
16
+ type: "webp";
17
+ quality: number;
18
+ maxWidth: number;
19
+ maxHeight: number;
20
+ };
21
+ export declare const ENCODE_DEFAULTS: Omit<EncodeSettings, "type">;
22
+ export declare const ENCODE_MIME_TYPE_OF: Record<EncodeSettings["type"], string>;
23
+ /**
24
+ * Formats an encoder cannot round-trip without losing something: SVG is vector,
25
+ * an animated GIF keeps only its first frame, and AVIF is already smaller than
26
+ * anything we would produce.
27
+ */
28
+ export declare const NEVER_ENCODED: string[];
29
+ /**
30
+ * The schema's own option first, then the gallery it references, then off.
31
+ *
32
+ * Mirrors how `ImageField` resolves `accept` and `directory`. A gallery-backed
33
+ * field (`s.image(galleryVal)`) serializes with EMPTY options, so without the
34
+ * gallery fallback it could never inherit what the gallery asked for — and
35
+ * `s.image(galleryVal, { encode: false })` is how such a field opts back out.
36
+ *
37
+ * Nonsense numbers fall back to the defaults rather than through. `fitWithin`
38
+ * reads a bound of 0 (or NaN, or a negative) as "everything already fits", so
39
+ * an unusable `maxWidth` would otherwise SILENTLY disable the downscale it was
40
+ * asking for, and a `quality` outside 0–1 is ignored by `canvas.toBlob` just as
41
+ * quietly. Falling back is deliberately not throwing: a schema is evaluated by
42
+ * the CLI's `node:vm` sandbox and by the Next server, so a typo'd quality would
43
+ * take a whole project down over an upload-time optimisation.
44
+ */
45
+ export declare function resolveEncodeSettings(fieldOption: ImageEncodeOption | undefined, galleryOption: ImageEncodeOption | undefined): EncodeSettings | null;
46
+ /**
47
+ * Dimensions scaled to fit inside the box, or null when the image already does.
48
+ *
49
+ * Null is the signal for "no downscale was needed", which the size comparison
50
+ * in {@link chooseEncoded} turns on.
51
+ */
52
+ export declare function fitWithin(width: number, height: number, maxWidth: number, maxHeight: number): {
53
+ width: number;
54
+ height: number;
55
+ } | null;
56
+ /**
57
+ * Whether a schema that declares `accept` would take this mime type.
58
+ *
59
+ * The same rules as `ImageSchema.executeValidate`, which validates the STORED
60
+ * mimeType against `accept` — so converting to a type `accept` forbids would
61
+ * upload a file the schema reports as invalid the moment it lands. Delegated to
62
+ * `@valbuild/core` rather than re-spelled, because a second reading of one
63
+ * `accept` string is how a converted image passes here and fails there.
64
+ */
65
+ export declare function isMimeTypeAccepted(mimeType: string, accept: string | undefined): boolean;
66
+ /** Formats left alone: the lossy list, plus the target type when it already fits. */
67
+ export declare function isSkippedSource(sourceMimeType: string, targetMimeType: string, needsDownscale: boolean): boolean;
68
+ /**
69
+ * Whether to upload what the encoder produced, or the file that was picked.
70
+ *
71
+ * Two ways the encoded bytes lose. They are not actually the type we asked for
72
+ * — `canvas.toBlob` answers an unsupported type with a PNG rather than with
73
+ * null, so "not null" is not the same question as "is a webp". Or they are
74
+ * simply bigger, which happens for small and flat images: measured, an 8x8
75
+ * solid PNG of 74 bytes becomes a 548 byte webp. A downscale overrides that,
76
+ * since the original is then the wrong size whatever it weighs.
77
+ */
78
+ export declare function chooseEncoded(input: {
79
+ originalSize: number;
80
+ encodedSize: number | null;
81
+ encodedType: string | null;
82
+ targetMimeType: string;
83
+ needsDownscale: boolean;
84
+ }): boolean;
85
+ /**
86
+ * `photo.png` -> `photo.webp`. The extension is re-derived downstream too, but
87
+ * the name should not lie.
88
+ */
89
+ export declare function withExtension(filename: string, ext: string): string;