@valbuild/server 0.121.0 → 0.122.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,137 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.122.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+
9
+ Val could publish edits but never look back. Now every commit is a durable
10
+ record you can open, and any part of it can be put back.
11
+
12
+ **Open a commit and the Studio splits in two.** The left half is the Studio
13
+ itself — same navigation, same fields, same everything, because it _is_ the
14
+ editor rather than a copy of it. The right half shows the project as that commit
15
+ left it. On a phone the two become one pane and a toggle.
16
+
17
+ **Restoring is directed: you point at the old value, then at where it goes.**
18
+ Val does not try to work out which of today's fields corresponds to which of the
19
+ commit's. It cannot be sure — array items splice, schemas move — and a restore
20
+ that guesses wrong writes into the wrong place and looks like it worked. Two
21
+ picks leave nothing to guess. You can restore across paths, so last month's
22
+ headline can become today's tagline.
23
+
24
+ Before you click, every field on the "now" side says whether it can hold the
25
+ value you picked, and a field that cannot explains why when you click it rather
26
+ than doing nothing. A changed union is not itself a blocker: what matters is
27
+ whether the value's own shape is still allowed, so a union that gained a case
28
+ restores fine and one that lost the case you are restoring does not.
29
+
30
+ Rich text can be restored but is marked "probably fits" rather than confirmed —
31
+ comparing every mark and block against the options a schema allows is not done
32
+ yet, and saying so is better than a confident answer we cannot back. It is
33
+ checked properly the moment you commit to it: before anything is staged, the old
34
+ value is checked against the field it is going into, and a value that cannot be
35
+ that field is refused with the reason. A value that is the right shape but
36
+ breaks a rule about its content — a name too short for its `minLength` — is
37
+ staged and then held at publish, the same as if you had typed it, because a
38
+ restore should not be stricter than typing.
39
+
40
+ **A whole module can be put back on its own**, from a commit that changed
41
+ several, without reverting the rest of the commit.
42
+
43
+ **Restores are staged, not applied.** They land in pending changes, are reviewed
44
+ beside every other edit, and go out with the next publish. There is also "put
45
+ everything back", for when a whole publish was the mistake.
46
+
47
+ To make this possible, publishing now records each changed module's data and the
48
+ schema it was written against. Not the `.val.ts` — git already keeps that, but
49
+ it is code, and turning code back into data means parsing it, which is
50
+ best-effort and stops working as TypeScript, your runtime and Val move on. The
51
+ schema is kept because a value on its own cannot be drawn: showing a module as it
52
+ was at a commit whose schema has since changed needs _that commit's_ schema, and
53
+ nothing in your current checkout has it.
54
+
55
+ Things it will not pretend about: a module the commit did not touch says so
56
+ rather than showing today's value; a module saved by a different version of Val
57
+ says the version differs and that nothing is lost; a commit made before Val
58
+ started recording history disables restore with the reason next to it. Images
59
+ and files are restored by re-uploading them, since the bytes at an old commit
60
+ may no longer be on your branch.
61
+
62
+ History requires the Val content service. In filesystem mode it reports
63
+ `not-supported-in-fs-mode` rather than faking it from git, which has the files
64
+ but not which of a commit's changes were one editor's work.
65
+
66
+ - [#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`
67
+ config, or local filesystem mode.
68
+
69
+ Until now, an MCP endpoint with no `oauth` config accepted whatever bearer token
70
+ a caller presented and relayed it to the Val content backend unread. The
71
+ reasoning was that without an issuer the app has no key to check a token
72
+ against, so it should not pretend to be the authority on what that token may
73
+ do — and that much was right. The shape was not: a credential the app cannot
74
+ check is one it cannot refuse either, so "a deployed endpoint that authenticates
75
+ nobody" was a supported configuration, and an app could serve content-rewriting
76
+ tools without ever being told where its callers should authorize.
77
+
78
+ **If you run Val in proxy mode**, MCP now requires the `oauth` config that
79
+ shipped in `0.120.0`. Callers authorize as themselves against the Val
80
+ authorization server, this app verifies the token's signature, issuer, audience
81
+ and expiry itself, and patches carry the verified profile as their author:
82
+
83
+ ```ts
84
+ initValMcp(valModules, config, {
85
+ oauth: {
86
+ issuer: "https://admin.val.build",
87
+ resource: "https://your-app.com/api/mcp",
88
+ },
89
+ });
90
+ ```
91
+
92
+ Leave it out and the endpoint answers `500` naming the missing config, rather
93
+ than serving the request.
94
+
95
+ **If you run Val in local filesystem mode**, nothing changes. Local development
96
+ still needs no `oauth` config and no authorization server: there is no backend
97
+ to authenticate to, and patches are written with no author. A token presented
98
+ to such a project is still refused rather than ignored — the endpoint answers
99
+ `400` and says to take the credential out of the client's configuration, since
100
+ what it reached was a working tree with no permission check in front of it.
101
+
102
+ Two API changes if you built your own host on `createValTools`:
103
+
104
+ - `ValToolContext.auth` no longer has a `{ type: "pat", pat }` variant.
105
+ `{ type: "verified-profile", profileId, scopes }` is the only credential the
106
+ registry accepts, and `null` still means local filesystem mode.
107
+ - `createValOps` no longer takes an `auth` argument. `ValOpsHttp` still accepts
108
+ a personal access token directly — that is how `val debug` uses the token from
109
+ `val login` — but no server request builds one.
110
+
111
+ Proxy mode also stops keeping one data layer per credential. Each personal
112
+ access token needed its own `ValOpsHttp` to hold it, each of those cached the
113
+ project's evaluated modules, and the bounded cache that kept the memory in
114
+ check turned an eviction into a re-evaluation of every module on the next call.
115
+ Verified callers all share one instance, because they all reach the backend
116
+ under the app's own API key.
117
+
118
+ ### Patch Changes
119
+
120
+ - [#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.
121
+
122
+ Nothing called it. The Studio reads the profile id from `/stat`, and in proxy
123
+ mode the route proxied to `${VAL_BUILD_URL}/api/val/${project}/auth/session`,
124
+ an upstream route that no longer exists — so calling it by hand returned a 500
125
+ rather than a session. It is gone from both the route declarations in
126
+ `@valbuild/shared` and the implementation in `@valbuild/server`.
127
+
128
+ Session cookie handling itself is unchanged: `/authorize`, `/callback` and
129
+ `/logout` still set and clear `val_session` as before.
130
+
131
+ - 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)]:
132
+ - @valbuild/shared@0.122.0
133
+ - @valbuild/ui@0.122.0
134
+
3
135
  ## 0.121.0
4
136
 
5
137
  ### 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 {};