@valbuild/server 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,229 @@
1
1
  # @valbuild/server
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
+ ### Patch Changes
90
+
91
+ - Updated dependencies [[`c5e7dfd`](https://github.com/valbuild/val/commit/c5e7dfd12aa1371195be642e1c2fb72b6f3e3ce2), [`53f670c`](https://github.com/valbuild/val/commit/53f670c0cf2d7a03a6d068c78b7874ce77652c2a)]:
92
+ - @valbuild/shared@0.123.0
93
+ - @valbuild/ui@0.123.0
94
+
95
+ ## 0.122.0
96
+
97
+ ### Minor Changes
98
+
99
+ - [#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
100
+
101
+ Val could publish edits but never look back. Now every commit is a durable
102
+ record you can open, and any part of it can be put back.
103
+
104
+ **Open a commit and the Studio splits in two.** The left half is the Studio
105
+ itself — same navigation, same fields, same everything, because it _is_ the
106
+ editor rather than a copy of it. The right half shows the project as that commit
107
+ left it. On a phone the two become one pane and a toggle.
108
+
109
+ **Restoring is directed: you point at the old value, then at where it goes.**
110
+ Val does not try to work out which of today's fields corresponds to which of the
111
+ commit's. It cannot be sure — array items splice, schemas move — and a restore
112
+ that guesses wrong writes into the wrong place and looks like it worked. Two
113
+ picks leave nothing to guess. You can restore across paths, so last month's
114
+ headline can become today's tagline.
115
+
116
+ Before you click, every field on the "now" side says whether it can hold the
117
+ value you picked, and a field that cannot explains why when you click it rather
118
+ than doing nothing. A changed union is not itself a blocker: what matters is
119
+ whether the value's own shape is still allowed, so a union that gained a case
120
+ restores fine and one that lost the case you are restoring does not.
121
+
122
+ Rich text can be restored but is marked "probably fits" rather than confirmed —
123
+ comparing every mark and block against the options a schema allows is not done
124
+ yet, and saying so is better than a confident answer we cannot back. It is
125
+ checked properly the moment you commit to it: before anything is staged, the old
126
+ value is checked against the field it is going into, and a value that cannot be
127
+ that field is refused with the reason. A value that is the right shape but
128
+ breaks a rule about its content — a name too short for its `minLength` — is
129
+ staged and then held at publish, the same as if you had typed it, because a
130
+ restore should not be stricter than typing.
131
+
132
+ **A whole module can be put back on its own**, from a commit that changed
133
+ several, without reverting the rest of the commit.
134
+
135
+ **Restores are staged, not applied.** They land in pending changes, are reviewed
136
+ beside every other edit, and go out with the next publish. There is also "put
137
+ everything back", for when a whole publish was the mistake.
138
+
139
+ To make this possible, publishing now records each changed module's data and the
140
+ schema it was written against. Not the `.val.ts` — git already keeps that, but
141
+ it is code, and turning code back into data means parsing it, which is
142
+ best-effort and stops working as TypeScript, your runtime and Val move on. The
143
+ schema is kept because a value on its own cannot be drawn: showing a module as it
144
+ was at a commit whose schema has since changed needs _that commit's_ schema, and
145
+ nothing in your current checkout has it.
146
+
147
+ Things it will not pretend about: a module the commit did not touch says so
148
+ rather than showing today's value; a module saved by a different version of Val
149
+ says the version differs and that nothing is lost; a commit made before Val
150
+ started recording history disables restore with the reason next to it. Images
151
+ and files are restored by re-uploading them, since the bytes at an old commit
152
+ may no longer be on your branch.
153
+
154
+ History requires the Val content service. In filesystem mode it reports
155
+ `not-supported-in-fs-mode` rather than faking it from git, which has the files
156
+ but not which of a commit's changes were one editor's work.
157
+
158
+ - [#597](https://github.com/valbuild/val/pull/597) [`5d14612`](https://github.com/valbuild/val/commit/5d14612f612d657a37338136188f2b3c02b28fe7) Thanks [@freekh](https://github.com/freekh)! - MCP: remove personal access token auth. The endpoint now needs an `oauth`
159
+ config, or local filesystem mode.
160
+
161
+ Until now, an MCP endpoint with no `oauth` config accepted whatever bearer token
162
+ a caller presented and relayed it to the Val content backend unread. The
163
+ reasoning was that without an issuer the app has no key to check a token
164
+ against, so it should not pretend to be the authority on what that token may
165
+ do — and that much was right. The shape was not: a credential the app cannot
166
+ check is one it cannot refuse either, so "a deployed endpoint that authenticates
167
+ nobody" was a supported configuration, and an app could serve content-rewriting
168
+ tools without ever being told where its callers should authorize.
169
+
170
+ **If you run Val in proxy mode**, MCP now requires the `oauth` config that
171
+ shipped in `0.120.0`. Callers authorize as themselves against the Val
172
+ authorization server, this app verifies the token's signature, issuer, audience
173
+ and expiry itself, and patches carry the verified profile as their author:
174
+
175
+ ```ts
176
+ initValMcp(valModules, config, {
177
+ oauth: {
178
+ issuer: "https://admin.val.build",
179
+ resource: "https://your-app.com/api/mcp",
180
+ },
181
+ });
182
+ ```
183
+
184
+ Leave it out and the endpoint answers `500` naming the missing config, rather
185
+ than serving the request.
186
+
187
+ **If you run Val in local filesystem mode**, nothing changes. Local development
188
+ still needs no `oauth` config and no authorization server: there is no backend
189
+ to authenticate to, and patches are written with no author. A token presented
190
+ to such a project is still refused rather than ignored — the endpoint answers
191
+ `400` and says to take the credential out of the client's configuration, since
192
+ what it reached was a working tree with no permission check in front of it.
193
+
194
+ Two API changes if you built your own host on `createValTools`:
195
+
196
+ - `ValToolContext.auth` no longer has a `{ type: "pat", pat }` variant.
197
+ `{ type: "verified-profile", profileId, scopes }` is the only credential the
198
+ registry accepts, and `null` still means local filesystem mode.
199
+ - `createValOps` no longer takes an `auth` argument. `ValOpsHttp` still accepts
200
+ a personal access token directly — that is how `val debug` uses the token from
201
+ `val login` — but no server request builds one.
202
+
203
+ Proxy mode also stops keeping one data layer per credential. Each personal
204
+ access token needed its own `ValOpsHttp` to hold it, each of those cached the
205
+ project's evaluated modules, and the bounded cache that kept the memory in
206
+ check turned an eviction into a re-evaluation of every module on the next call.
207
+ Verified callers all share one instance, because they all reach the backend
208
+ under the app's own API key.
209
+
210
+ ### Patch Changes
211
+
212
+ - [#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.
213
+
214
+ Nothing called it. The Studio reads the profile id from `/stat`, and in proxy
215
+ mode the route proxied to `${VAL_BUILD_URL}/api/val/${project}/auth/session`,
216
+ an upstream route that no longer exists — so calling it by hand returned a 500
217
+ rather than a session. It is gone from both the route declarations in
218
+ `@valbuild/shared` and the implementation in `@valbuild/server`.
219
+
220
+ Session cookie handling itself is unchanged: `/authorize`, `/callback` and
221
+ `/logout` still set and clear `val_session` as before.
222
+
223
+ - Updated dependencies [[`be32261`](https://github.com/valbuild/val/commit/be32261af19db8018bc37b180d903416018c0b79), [`da6794f`](https://github.com/valbuild/val/commit/da6794f3dbd77d49ccfe780b359bab1689ee1b11), [`1c8b7fd`](https://github.com/valbuild/val/commit/1c8b7fda1e84cd8bd32a03a85d2789598b98c3fb)]:
224
+ - @valbuild/shared@0.122.0
225
+ - @valbuild/ui@0.122.0
226
+
3
227
  ## 0.121.0
4
228
 
5
229
  ### Minor Changes
@@ -1,6 +1,8 @@
1
1
  import { MediaSource, FileMetadata, FileSource, ImageMetadata, ModuleFilePath, PatchId, Schema, SelectorSource, SerializedSchema, Source, SourcePath, ValConfig, ValModules, ValidationError } from "@valbuild/core";
2
2
  import { result } from "@valbuild/core/fp";
3
3
  import { JSONValue, ParentRef, Patch, PatchError } from "@valbuild/core/patch";
4
+ import type { HistoryError } from "./history/HistoryError.js";
5
+ import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
4
6
  import { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
5
7
  import { ParentPatchId } from "@valbuild/core";
6
8
  import type { ReifiedPreview } from "@valbuild/core";
@@ -408,10 +410,32 @@ export declare abstract class ValOps {
408
410
  protected abstract getSourceFile(path: string): Promise<WithGenericError<{
409
411
  data: string;
410
412
  }>>;
413
+ /**
414
+ * Save a patch's binary file from a `data:...;base64,...` URL.
415
+ *
416
+ * The wire form: `FileReader.readAsDataURL` is what the browser produces, and
417
+ * published `@valbuild/server` versions send it. Code that already HAS bytes
418
+ * should call {@link saveBinaryFileFromPatch} instead of wrapping them in a
419
+ * data URL just to have this unwrap them again.
420
+ *
421
+ * A `null` `data` records a DELETION, which is why this cannot simply be
422
+ * replaced by the byte-taking sibling: there is nothing to hand it.
423
+ */
411
424
  abstract saveBase64EncodedBinaryFileFromPatch(filePath: string, parentRef: ParentRef, patchId: PatchId, data: string | null, type: "file" | "image", metadata: MetadataOfType<"file" | "image"> | undefined): Promise<WithGenericError<{
412
425
  patchId: PatchId;
413
426
  filePath: string;
414
427
  }>>;
428
+ /**
429
+ * The same, for a caller that already has the bytes.
430
+ *
431
+ * Default implementation wraps them back into a data URL so every backend
432
+ * gets this for free; a backend that can take bytes straight through should
433
+ * override it.
434
+ */
435
+ saveBinaryFileFromPatch(filePath: string, parentRef: ParentRef, patchId: PatchId, bytes: Buffer, mimeType: string, type: "file" | "image", metadata: MetadataOfType<"file" | "image"> | undefined): Promise<WithGenericError<{
436
+ patchId: PatchId;
437
+ filePath: string;
438
+ }>>;
415
439
  abstract getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId, remote: boolean): Promise<Buffer | null>;
416
440
  protected abstract getBase64EncodedBinaryFileMetadataFromPatch<T extends "file" | "image">(filePath: string, type: T, patchId: PatchId, remote: boolean): Promise<OpsMetadata<T>>;
417
441
  abstract getBinaryFile(filePathOrRef: string): Promise<Buffer | null>;
@@ -428,6 +452,40 @@ export declare abstract class ValOps {
428
452
  errors?: undefined;
429
453
  deleted?: undefined;
430
454
  }>;
455
+ /** One page of a branch's commits, newest first. See history/listCommits. */
456
+ abstract listCommits(branch: string, options?: {
457
+ limit?: number;
458
+ cursor?: string;
459
+ }): Promise<result.Result<CommitPage, HistoryError>>;
460
+ /** The patches that produced one commit, with their ops. */
461
+ abstract getCommitPatches(commitSha: string): Promise<result.Result<{
462
+ commit: HistoricalCommit;
463
+ patches: CommitPatch[];
464
+ }, HistoryError>>;
465
+ /**
466
+ * How each `.val.ts` the commit changed looked BEFORE it, keyed by module
467
+ * file path. Empty for a commit made before this was recorded - which the
468
+ * caller reports as `source-unavailable` rather than as an empty module.
469
+ */
470
+ /**
471
+ * Each module a commit changed: its data, and the schema it was under.
472
+ *
473
+ * `asOf` widens it from "what this commit changed" to "the whole project as
474
+ * this commit left it", which is what reverting everything to a point in time
475
+ * needs; `moduleFilePath` narrows it to one module, for navigating the
476
+ * history pane off the changed set.
477
+ */
478
+ abstract getCommitModules(commitSha: string, options?: {
479
+ asOf?: boolean;
480
+ moduleFilePath?: ModuleFilePath;
481
+ }): Promise<result.Result<{
482
+ modules: StoredModuleVersion[];
483
+ complete: boolean;
484
+ }, HistoryError>>;
485
+ /** Which files the commit touched, and how. Names them; does not fetch them. */
486
+ abstract getCommitAffectedFiles(commitSha: string): Promise<result.Result<AffectedFile[], HistoryError>>;
487
+ /** One file's bytes as they were at one commit. */
488
+ abstract getFileAtCommit(commitSha: string, filePath: string, remote: boolean): Promise<result.Result<Buffer, HistoryError>>;
431
489
  }
432
490
  export type WithGenericError<T extends Record<string, unknown>> = (T & {
433
491
  error?: undefined;
@@ -533,6 +591,15 @@ export type PreparedCommit = {
533
591
  * Previous source files that were patched
534
592
  */
535
593
  previousSourceFiles: Record<ModuleFilePath, string>;
594
+ /**
595
+ * Each changed module's Source after this commit, and its schema.
596
+ *
597
+ * This is what makes a commit restorable. See the comment where it is built.
598
+ */
599
+ moduleVersions: Record<ModuleFilePath, {
600
+ source: JSONValue | null;
601
+ schema: SerializedSchema;
602
+ }>;
536
603
  /**
537
604
  * Diagnosis only: what the source file looks like with the appliable patches
538
605
  * applied, for modules that had at least one unappliable patch. Populated
@@ -640,6 +707,5 @@ export type OrderedPatchesMetadata = {
640
707
  };
641
708
  export declare function getFieldsForType<T extends BinaryFileType>(type: T): (keyof MetadataOfType<T> & string)[];
642
709
  export declare function createMetadataFromBuffer<T extends BinaryFileType>(type: BinaryFileType, mimeType: string, buffer: Buffer): OpsMetadata<T>;
643
- export declare function getMimeTypeFromBase64(content: string): string | null;
644
710
  export declare function guessMimeTypeFromPath(filePath: string): string | null;
645
711
  export declare function bufferFromDataUrl(dataUrl: string): Buffer | undefined;
@@ -1,6 +1,9 @@
1
1
  import { PatchId, ModuleFilePath, ValModules } from "@valbuild/core";
2
2
  import { AuthorId, BaseSha, BinaryFileType, 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
+ import type { HistoryError } from "./history/HistoryError.js";
5
+ import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
6
+ import { result } from "@valbuild/core/fp";
4
7
  import { Buffer } from "buffer";
5
8
  export declare class ValOpsFS extends ValOps {
6
9
  private readonly contentUrl;
@@ -158,4 +161,15 @@ export declare class ValOpsFS extends ValOps {
158
161
  * whole directory, and a lock that moves away with it is not holding anything.
159
162
  */
160
163
  private getPatchLockFile;
164
+ listCommits(): Promise<result.Result<CommitPage, HistoryError>>;
165
+ getCommitPatches(): Promise<result.Result<{
166
+ commit: HistoricalCommit;
167
+ patches: CommitPatch[];
168
+ }, HistoryError>>;
169
+ getCommitModules(): Promise<result.Result<{
170
+ modules: StoredModuleVersion[];
171
+ complete: boolean;
172
+ }, HistoryError>>;
173
+ getCommitAffectedFiles(): Promise<result.Result<AffectedFile[], HistoryError>>;
174
+ getFileAtCommit(): Promise<result.Result<Buffer, HistoryError>>;
161
175
  }
@@ -2,7 +2,10 @@ import { type PatchId, type ModuleFilePath, ValModules } from "@valbuild/core";
2
2
  import type { Patch as PatchT, ParentRef as ParentRefT } from "@valbuild/core/patch";
3
3
  import { type AuthorId, type BaseSha, BinaryFileType, type CommitSha, GenericErrorMessage, MetadataOfType, OpsMetadata, PreparedCommit, ValOps, ValOpsOptions, WithGenericError, SaveSourceFilePatchResult, type PatchGroupMembership, SchemaSha, OrderedPatchesMetadata, OrderedPatches, SourcesSha } from "./ValOps.js";
4
4
  import { z } from "zod";
5
+ import type { HistoryError } from "./history/HistoryError.js";
6
+ import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
5
7
  import { ParentRef, ValCommit, ValDeployment, type PatchGroupT } from "@valbuild/shared/internal";
8
+ import { result } from "@valbuild/core/fp";
6
9
  declare const PatchId: z.ZodString & z.ZodType<PatchId, string, z.core.$ZodTypeInternals<PatchId, string>>;
7
10
  declare const CommitSha: z.ZodString & z.ZodType<CommitSha, string, z.core.$ZodTypeInternals<CommitSha, string>>;
8
11
  declare const BaseSha: z.ZodString & z.ZodType<BaseSha, string, z.core.$ZodTypeInternals<BaseSha, string>>;
@@ -281,5 +284,32 @@ export declare class ValOpsHttp extends ValOps {
281
284
  isNotFastForward?: boolean;
282
285
  error: GenericErrorMessage;
283
286
  }>;
287
+ /**
288
+ * One GET against the content service, parsed and Result-typed.
289
+ *
290
+ * Every history read has the same three failure modes - could not reach the
291
+ * service, the commit is not there, the answer was not what was expected -
292
+ * and each of them means something different to a caller deciding whether to
293
+ * offer a restore. Doing it once here is what keeps that consistent across
294
+ * the five endpoints.
295
+ */
296
+ private getHistory;
297
+ listCommits(branch: string, options?: {
298
+ limit?: number;
299
+ cursor?: string;
300
+ }): Promise<result.Result<CommitPage, HistoryError>>;
301
+ getCommitPatches(commitSha: string): Promise<result.Result<{
302
+ commit: HistoricalCommit;
303
+ patches: CommitPatch[];
304
+ }, HistoryError>>;
305
+ getCommitModules(commitSha: string, options?: {
306
+ asOf?: boolean;
307
+ moduleFilePath?: ModuleFilePath;
308
+ }): Promise<result.Result<{
309
+ modules: StoredModuleVersion[];
310
+ complete: boolean;
311
+ }, HistoryError>>;
312
+ getCommitAffectedFiles(commitSha: string): Promise<result.Result<AffectedFile[], HistoryError>>;
313
+ getFileAtCommit(commitSha: string, filePath: string, remote: boolean): Promise<result.Result<Buffer, HistoryError>>;
284
314
  }
285
315
  export {};