@valbuild/server 0.129.0 → 0.131.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,172 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.131.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#686](https://github.com/valbuild/val/pull/686) [`0d5857b`](https://github.com/valbuild/val/commit/0d5857b731e11f7e6a011f79297df6485908c31f) Thanks [@freekh](https://github.com/freekh)! - Say `VAL_MODE=memory` where there is no disk, and get a sentence instead of an `EPERM`
8
+
9
+ Memory mode — the one for a host that holds the project's source itself — is
10
+ selected by passing `sourceFiles`, and it has to be: nothing in an environment
11
+ can supply a project's source, so a mode that an env var could switch on would
12
+ be a server with no content in it.
13
+
14
+ The cost was the failure when a host forgot. Val inferred `fs` mode, `fs` mode
15
+ went looking for a working tree, and in a Worker isolate the first thing to
16
+ touch the disk failed:
17
+
18
+ ```
19
+ patch-error /bundle/.val/patches.lock: EPERM
20
+ ```
21
+
22
+ That names a path two layers below the decision that caused it, and nobody
23
+ reading it would guess "your server was configured for the wrong mode".
24
+
25
+ So an environment can now DECLARE that it has no disk:
26
+
27
+ ```
28
+ VAL_MODE=memory
29
+ ```
30
+
31
+ It does not turn memory mode on. It says the host is supposed to be supplying
32
+ `sourceFiles`, so if none arrive, Val refuses at configuration time and says
33
+ where to pass them. `VAL_MODE=` counts as unset, the way a shell means it; any
34
+ other value is refused rather than ignored, since leaving you in `fs` mode is
35
+ the exact failure this is meant to catch.
36
+
37
+ **`initValContent` takes the same options, and this is the release that
38
+ noticed.** It builds a Val server of its own — these readers resolve content by
39
+ asking it, not by calling the API over HTTP — so configuring `initValServer`
40
+ alone left them inferring `fs` mode. On a host with no filesystem that is a
41
+ reader looking for a working tree that is not there; it went unnoticed because
42
+ published reads still worked.
43
+
44
+ ```ts
45
+ const patchStore = new InMemoryPatchStore(); // now exported from this package
46
+
47
+ const { valApiHandler, draftMode } = initValServer(valModules, config, {
48
+ sourceFiles: FILES,
49
+ patchStore,
50
+ unsafelyAllowUnauthenticated: true,
51
+ });
52
+
53
+ const { fetchValStega } = initValContent(config, valModules, {
54
+ draftMode,
55
+ // The same three. Two patch stores are two sets of pending edits, and a
56
+ // reader that checks a session the host never issues answers itself 401 and
57
+ // falls back to published content — a draft render showing the live site.
58
+ sourceFiles: FILES,
59
+ patchStore,
60
+ unsafelyAllowUnauthenticated: true,
61
+ });
62
+ ```
63
+
64
+ All three are optional. Left out, this reader gets its own store and its own
65
+ answer about authentication, which is right for published content.
66
+
67
+ `@valbuild/next` has no memory mode: its `initValServer` takes neither option,
68
+ so for a Next app `VAL_MODE=memory` names an environment Val cannot serve from,
69
+ and the error says so.
70
+
71
+ Nothing changes for an app that sets none of this: `http` when `VAL_API_KEY`
72
+ and `VAL_SECRET` are both present, `fs` otherwise, as before.
73
+
74
+ ## 0.130.0
75
+
76
+ ### Minor Changes
77
+
78
+ - [#684](https://github.com/valbuild/val/pull/684) [`cab4098`](https://github.com/valbuild/val/commit/cab4098969585977b8d7574e86d66fcb01cb1d75) Thanks [@freekh](https://github.com/freekh)! - A third ValOps mode, for a host that already holds its own source
79
+
80
+ EXPERIMENTAL. `fs` mode assumes a working tree it can watch and write; `http`
81
+ mode assumes Val's content service owns the patch chain and that a commit is a
82
+ git commit. A host that builds and publishes its own output is neither: it holds
83
+ the source already, it has nowhere to watch, and its "commit" is a new build.
84
+
85
+ Forcing such a host into `fs` mode cost three things, all now fixed: `/stat`
86
+ long-polled against watchers that could never fire, burning CPU for the whole
87
+ hold to learn nothing;
88
+ `/api/val/enable` 500'd; and every read of a `.val.ts` went through a shimmed
89
+ filesystem when the host could simply hand the source over.
90
+
91
+ `ValOpsMemory` takes the source as `sourceFiles`, refuses the local binary
92
+ members by name — this configuration uses Val's remote files — and answers the
93
+ history members with the same closed `not-supported-in-fs-mode` error `ValOpsFS`
94
+ gives, so the History UI degrades the way it already knows how rather than
95
+ inventing a commit list.
96
+
97
+ `getStat` still long-polls -- the hold is what paces the client, and an earlier
98
+ version that answered immediately turned a 20-second poll into a request every
99
+ 6ms -- but it parks on a SIGNAL rather than a timer. This mode owns its store,
100
+ so it is told when something changes: no timers while parked, and a patch
101
+ written by another tab is seen at once rather than up to 250ms later.
102
+
103
+ Two seams come with it. `commitPrepared` lets a host take what a save produced
104
+ instead of a git commit, and `publishOverride` lets a publish be something other
105
+ than a push. Both are opt-in; an app that sets neither behaves exactly as before.
106
+
107
+ The in-memory patch store is explicitly **not durable**. It is behind
108
+ `ValPatchStore`, so a durable implementation is a swap rather than a rewrite,
109
+ but as shipped a restart loses unpublished patches.
110
+
111
+ **Memory mode authenticates.** `ValOps` gained `requiresAuth` alongside
112
+ `patchesAreLocal`, because one flag was answering two questions: whether a store
113
+ auto-saves or publishes (behaviour, reported as `mode` and keyed off by the UI),
114
+ and whether an unauthenticated request may write (security). With two
115
+ implementations the answers coincided — `fs` is a developer's own machine where
116
+ no credential exists, `http` is remote — so `getAuth` was written against
117
+ `patchesAreLocal` and returned anonymous _success_ for a missing cookie, an
118
+ invalid JWT, an unparseable payload, or no configured secret.
119
+
120
+ Memory mode splits them: its store is local, and it runs deployed. It therefore
121
+ requires a verified session, like `http` mode. A host that authorises requests
122
+ before Val sees them can opt out with `unsafelyAllowUnauthenticated`, which is
123
+ spelled that way on purpose and warns at startup. `fs` mode is unchanged.
124
+
125
+ Val's own MCP endpoint refuses memory mode outright. It has the same absence fs
126
+ mode has — no credential, no backend, every permission check on the far side of
127
+ one — and unlike fs mode it is meant to run deployed, so the existing
128
+ "development only" and loopback guards refuse nothing. A host in this mode owns
129
+ its own trust boundary and can offer the tools through it.
130
+
131
+ Internally, the routes' `instanceof ValOpsFS` checks meant "is this a local
132
+ store" — correct with two implementations and silently wrong with three. They are
133
+ now `ValOps.patchesAreLocal` at all 17 policy sites.
134
+
135
+ ### Patch Changes
136
+
137
+ - [#684](https://github.com/valbuild/val/pull/684) [`cab4098`](https://github.com/valbuild/val/commit/cab4098969585977b8d7574e86d66fcb01cb1d75) Thanks [@freekh](https://github.com/freekh)! - Stop telling people to run `val login` where a personal access token cannot be used
138
+
139
+ A PAT is read from a file in the _server's_ working directory, and only local
140
+ `fs` mode has one. `resolveRemoteFileAuth` knew that; two things upstream did not.
141
+
142
+ `RemoteFilesErrorDialog` was unconditional. Whatever went wrong with remote files,
143
+ it said "Personal access token file required" and told the reader to run a command
144
+ in their project root — for a server with no working directory, a directory that
145
+ does not exist, to produce a file it could not read. The reason was already on the
146
+ error object and simply never looked at. The dialog now shows only for the two
147
+ reasons a PAT can actually fix, and everything else gets its own message.
148
+
149
+ `resolveRemoteFileAuth` also answered `project-not-configured` for a non-fs mode
150
+ with no api key, which is wrong twice: the project may be configured perfectly
151
+ well, and it is the credential that is absent. It answers `api-key-missing` now,
152
+ already in the wire contract, and that message no longer says "production mode",
153
+ because every server that is not local dev gives it.
154
+
155
+ - [#684](https://github.com/valbuild/val/pull/684) [`cab4098`](https://github.com/valbuild/val/commit/cab4098969585977b8d7574e86d66fcb01cb1d75) Thanks [@freekh](https://github.com/freekh)! - `createValApiRouter` no longer puts `fs` in every integration's module graph
156
+
157
+ `fs` and `path` were imported at module scope for `safeReadGit`, a local
158
+ development convenience that scans upwards for a `.git` to guess the commit and
159
+ branch, and whose only caller is the CLI. A static import put `fs` in the module
160
+ graph of everything reaching `createValApiRouter` — which is every server
161
+ integration, including ones that run where there is no filesystem at all.
162
+
163
+ Behaviour is unchanged where there is a filesystem.
164
+
165
+ - Updated dependencies [[`be1e8be`](https://github.com/valbuild/val/commit/be1e8bee673207596b3eb3d9a9886b8ade9b332f), [`8425378`](https://github.com/valbuild/val/commit/8425378c315ea46b5d822f1130b633e0449ff1b0), [`7d13dbc`](https://github.com/valbuild/val/commit/7d13dbced9ea49d8243b6b6cf9854cd1a259501f), [`cab4098`](https://github.com/valbuild/val/commit/cab4098969585977b8d7574e86d66fcb01cb1d75), [`07db94c`](https://github.com/valbuild/val/commit/07db94c23b73c8c0b2b50a30a89926823c2da1d6), [`473a185`](https://github.com/valbuild/val/commit/473a185f70351b44388f3bc1852649e2c1dbe001), [`64f0de3`](https://github.com/valbuild/val/commit/64f0de339b8621cb5a6c422dfe55cae5b2bbe2a0)]:
166
+ - @valbuild/ui@0.130.0
167
+ - @valbuild/core@0.130.0
168
+ - @valbuild/shared@0.130.0
169
+
3
170
  ## 0.129.0
4
171
 
5
172
  ### Patch Changes
@@ -162,7 +162,7 @@ export declare abstract class ValOps {
162
162
  * put content in the memo that is not what was written, which is worse than
163
163
  * being stale.
164
164
  */
165
- adoptCommittedSources(analysis: PatchAnalysis & OrderedPatches, preparedCommit: Pick<PreparedCommit, "patchedJsonEntries">): Promise<void>;
165
+ adoptCommittedSources(analysis: PatchAnalysis & OrderedPatches, preparedCommit: Pick<PreparedCommit, "patchedJsonEntries" | "patchedSourceFiles">): Promise<void>;
166
166
  /**
167
167
  * Adopt sources that have just been written to disk, and move the SHAs with
168
168
  * them.
@@ -401,6 +401,55 @@ export declare abstract class ValOps {
401
401
  } | {
402
402
  errorType: "patch-head-conflict";
403
403
  }>>;
404
+ /**
405
+ * Take the `.val.ts` text a commit produced as the new committed source.
406
+ *
407
+ * A no-op where {@link getSourceFile} reads something the commit already
408
+ * wrote — the disk in `fs` mode, the content service in `http` mode. Override
409
+ * it in a store that holds the source itself. `null` means the commit deleted
410
+ * the file.
411
+ */
412
+ protected adoptPatchedSourceFiles(_files: Record<string, string | null>): void;
413
+ /**
414
+ * Whether the patches live HERE, in this server, or in Val's content service.
415
+ *
416
+ * Almost everything the routes branch on comes from this one fact, which is
417
+ * why it is a named property rather than an `instanceof`. If this server owns
418
+ * the store then there is no content service to authenticate to (so an absent
419
+ * or unverifiable session is anonymous rather than a 401), no shared store for
420
+ * a patch group to separate authors in, no deployments to report, and a
421
+ * "publish" writes what the host does with it rather than pushing a commit. If
422
+ * it does not, every one of those is the content service's and this server is
423
+ * relaying.
424
+ *
425
+ * There were two implementations when the routes were written and `instanceof
426
+ * ValOpsFS` meant this; a third made that reading wrong in a way that compiles
427
+ * silently -- a store that is local, answers none of the checks, and gets the
428
+ * http path with no content service behind it.
429
+ *
430
+ * It is also what `/stat` reports as `mode`. The wire name predates the third
431
+ * implementation and names a class, but the question the client is asking is
432
+ * this one: does it auto-save and hide the account panel, or does it publish.
433
+ */
434
+ abstract readonly patchesAreLocal: boolean;
435
+ /**
436
+ * Whether a request must carry a session this server verified.
437
+ *
438
+ * Split out of {@link patchesAreLocal}, which was answering two questions at
439
+ * once. "Does this store auto-save or publish" is a BEHAVIOUR question, and
440
+ * it is what `/stat` reports and the UI keys off. "May an unauthenticated
441
+ * request write here" is a SECURITY question. With two implementations the
442
+ * answers coincided -- fs is local dev where no credential exists, http is
443
+ * remote -- so one flag served both and nothing noticed.
444
+ *
445
+ * A third implementation splits them. `ValOpsMemory`'s store is local, which
446
+ * makes the first answer yes, and it is designed to run DEPLOYED, which makes
447
+ * the second answer no. Reusing one flag gave a deployed host `getAuth`
448
+ * returning anonymous success for a missing cookie, an invalid JWT, an
449
+ * unparseable payload, or no configured secret -- on all 29 routes, including
450
+ * the ones that create patches and publish.
451
+ */
452
+ abstract readonly requiresAuth: boolean;
404
453
  abstract onInit(baseSha: BaseSha, schemaSha: SchemaSha): Promise<void>;
405
454
  abstract fetchPatches<ExcludePatchOps extends boolean>(filters: {
406
455
  patchIds?: PatchId[];
@@ -10,6 +10,13 @@ export declare class ValOpsFS extends ValOps {
10
10
  private readonly rootDir;
11
11
  private static readonly VAL_DIR;
12
12
  private readonly host;
13
+ /** The developer's own working tree. See {@link ValOps.patchesAreLocal}. */
14
+ readonly patchesAreLocal = true;
15
+ /**
16
+ * The developer's own machine, where there is no credential to require and
17
+ * nothing to protect it from. See {@link ValOps.requiresAuth}.
18
+ */
19
+ readonly requiresAuth = false;
13
20
  constructor(contentUrl: string, rootDir: string, valModules: ValModules, options?: ValOpsOptions);
14
21
  /**
15
22
  * Change detection for `.jsonValues()` entry files, which no sha can see (their
@@ -27,6 +27,10 @@ export declare class ValOpsHttp extends ValOps {
27
27
  private readonly branch;
28
28
  private readonly authHeaders;
29
29
  private readonly root;
30
+ /** Val's content service owns the store. See {@link ValOps.patchesAreLocal}. */
31
+ readonly patchesAreLocal = false;
32
+ /** See {@link ValOps.requiresAuth}. */
33
+ readonly requiresAuth = true;
30
34
  constructor(contentUrl: string, project: string, commitSha: string, // TODO: CommitSha
31
35
  branch: string,
32
36
  /**
@@ -0,0 +1,306 @@
1
+ import type { ModuleFilePath, PatchId, ValModules } from "@valbuild/core";
2
+ import type { Patch, ParentRef } from "@valbuild/shared/internal";
3
+ import { ValOps, type AuthorId, type BaseSha, type GenericErrorMessage, type MetadataOfType, type OpsMetadata, type OrderedPatches, type OrderedPatchesMetadata, type PatchGroupMembership, type PreparedCommit, type SaveSourceFilePatchResult, type SchemaSha, type SourcesSha, type ValOpsOptions, type WithGenericError } from "./ValOps.js";
4
+ import type { HistoryError } from "./history/HistoryError.js";
5
+ import type { AffectedFile, CommitPage, CommitPatch, HistoricalCommit, StoredModuleVersion } from "./history/types.js";
6
+ import { result } from "@valbuild/core/fp";
7
+ /**
8
+ * One stored patch. Everything the ordered chain needs and nothing else.
9
+ */
10
+ export type StoredPatch = {
11
+ patchId: PatchId;
12
+ path: ModuleFilePath;
13
+ patch: Patch;
14
+ authorId: AuthorId | null;
15
+ createdAt: string;
16
+ baseSha: BaseSha;
17
+ };
18
+ /**
19
+ * One pending binary file: an upload that has not been published yet.
20
+ *
21
+ * Keyed by the patch that carries it, exactly as `fs` mode keys the directory
22
+ * it writes to. The patch's own `file` op holds a hash, not the bytes, so these
23
+ * arrive on their own request and are joined up by `(patchId, filePath)`.
24
+ */
25
+ export type StoredFile = {
26
+ patchId: PatchId;
27
+ /**
28
+ * Where the file will live once published.
29
+ *
30
+ * For a LOCAL file that is `/public/val/photo.jpg`. For a REMOTE one it is
31
+ * the path INSIDE the ref, not the ref itself -- `splitRemoteRef` has already
32
+ * taken it apart by the time the bytes get here, and both readers are keyed
33
+ * the same way. This is why neither takes `remote` into account: the caller
34
+ * has resolved that before it asks.
35
+ */
36
+ filePath: string;
37
+ data: Buffer;
38
+ metadata: MetadataOfType<"file" | "image"> | undefined;
39
+ };
40
+ /**
41
+ * Where pending patches live.
42
+ *
43
+ * The point of the interface: `ValOpsMemory` is not tied to memory. The default
44
+ * implementation below is explicitly NOT durable -- it dies with the process,
45
+ * or in a Worker with the isolate -- and a durable one (a Durable Object, whose
46
+ * single-threaded execution is the lock Val's fs store builds out of a file)
47
+ * is a swap rather than a rewrite.
48
+ *
49
+ * ORDER IS THE CONTRACT. `list()` returns patch ids in the order they were
50
+ * written, and that order is the chain: entry i's parent is entry i-1. This is
51
+ * the same decision `ValOpsFS` makes with `patches.log` -- the server decides
52
+ * where a patch goes, and it goes last -- and it exists for the same reason: an
53
+ * order held in the patches themselves lets a client working from a stale view
54
+ * strand every patch behind a parent that never landed.
55
+ */
56
+ export interface ValPatchStore {
57
+ list(): Promise<PatchId[]>;
58
+ get(patchId: PatchId): Promise<StoredPatch | null>;
59
+ append(patch: StoredPatch): Promise<void>;
60
+ /** The patches AND every file they carry. */
61
+ delete(patchIds: PatchId[]): Promise<void>;
62
+ /**
63
+ * Hold an uploaded file until its patch is published or dropped.
64
+ *
65
+ * Uploads arrive BEFORE the patch record does -- the record's `file` op
66
+ * carries only a hash, so it would otherwise point at nothing. So a file for
67
+ * a patch id that does not exist yet is normal and must be accepted. `fs`
68
+ * mode stages these outside its store and moves them in when the record
69
+ * lands, because a directory of files with no `patch.json` is indistinguishable
70
+ * from a patch whose contents were lost, and its repair pass deletes those.
71
+ * Nothing sweeps this store, so the two-step is not needed here -- at the cost
72
+ * that bytes uploaded for a patch that is never recorded stay until the store
73
+ * is dropped.
74
+ */
75
+ putFile(file: StoredFile): Promise<void>;
76
+ getFile(patchId: PatchId, filePath: string): Promise<StoredFile | null>;
77
+ deleteFile(patchId: PatchId, filePath: string): Promise<void>;
78
+ /** Every file held for a patch, which is what the publish step uploads. */
79
+ filesOf(patchId: PatchId): Promise<StoredFile[]>;
80
+ }
81
+ /** The default store. Not durable, deliberately and visibly. */
82
+ export declare class InMemoryPatchStore implements ValPatchStore {
83
+ private readonly order;
84
+ private readonly byId;
85
+ list(): Promise<PatchId[]>;
86
+ get(patchId: PatchId): Promise<StoredPatch | null>;
87
+ append(patch: StoredPatch): Promise<void>;
88
+ delete(patchIds: PatchId[]): Promise<void>;
89
+ private static fileKey;
90
+ private readonly files;
91
+ putFile(file: StoredFile): Promise<void>;
92
+ getFile(patchId: PatchId, filePath: string): Promise<StoredFile | null>;
93
+ deleteFile(patchId: PatchId, filePath: string): Promise<void>;
94
+ filesOf(patchId: PatchId): Promise<StoredFile[]>;
95
+ }
96
+ export type ValOpsMemoryOptions = ValOpsOptions & {
97
+ /**
98
+ * The project's source, by path, as the host already holds it.
99
+ *
100
+ * This is why the mode exists. `fs` mode reads `.val.ts` off a disk, and a
101
+ * host that builds and publishes does not have one -- giving it a shimmed
102
+ * filesystem to read through is what produced `Cannot access 'fs' before
103
+ * initialization` and a `/stat` that long-polls watchers which cannot fire.
104
+ * Here the host simply hands the source over.
105
+ */
106
+ sourceFiles: Record<string, string>;
107
+ /** Where pending patches live. Defaults to memory; see ValPatchStore. */
108
+ patchStore?: ValPatchStore;
109
+ /**
110
+ * Serve without authenticating any request. Off by default.
111
+ *
112
+ * The name is the documentation. This mode runs deployed, so an
113
+ * unauthenticated server is one where anyone who can reach the port can
114
+ * create patches and drive a publish -- which is why the default is to
115
+ * require a verified session like `http` mode does.
116
+ *
117
+ * A host sets this when it has its OWN boundary in front of Val and is
118
+ * asserting that every request reaching here has already been authorised by
119
+ * it. That is a real configuration, and it is not one to arrive at by
120
+ * accident, so it is spelled out rather than inferred and it warns at
121
+ * startup.
122
+ */
123
+ unsafelyAllowUnauthenticated?: boolean;
124
+ /**
125
+ * Val's content host, for pushing remote files at publish.
126
+ *
127
+ * Only needed by {@link ValOpsMemory.uploadRemoteFiles}. A project with no
128
+ * `s.image()` never reaches it.
129
+ */
130
+ contentUrl?: string;
131
+ };
132
+ /**
133
+ * A `ValOps` for a host that is neither a developer's machine nor
134
+ * content.val.build.
135
+ *
136
+ * EXPERIMENTAL -- see VAL_PROMPT.md.
137
+ *
138
+ * `fs` mode assumes a working tree it can watch and write; `http` mode assumes
139
+ * the content API owns the patch chain and a commit means a git commit. A host
140
+ * that builds and publishes its own output is neither: it holds the source
141
+ * already, it has nowhere to watch, and its "commit" is a new build.
142
+ *
143
+ * What this deliberately does NOT do:
144
+ *
145
+ * - **No filesystem.** Source comes from `sourceFiles`, patches from a store.
146
+ * - **No watching.** `getStat` still long-polls -- the hold is what paces the
147
+ * client -- but it parks on a signal rather than racing a timer against an
148
+ * mtime poll that can never observe anything here. Nothing can edit files
149
+ * behind Val's back: source changes only when the host publishes, and that
150
+ * replaces the process.
151
+ * - **Pending binary files, but no PUBLISHED local ones.** An upload is held in
152
+ * the patch store like any other pending change, so the Studio can preview it
153
+ * before it is published. What this has no answer for is a file that is
154
+ * already published and served from a `/public` directory: this configuration
155
+ * uses Val's REMOTE files, where a published image lives on the content host
156
+ * and the source carries a URL. `getBinaryFile` answers `null` for those --
157
+ * a miss, not a fault -- and `getBinaryFileMetadata` refuses by name.
158
+ * - **No git history.** There is no repository here, so the history methods
159
+ * answer `not-supported-in-fs-mode` -- the same closed error `ValOpsFS`
160
+ * uses, so the History UI degrades the way it already knows how rather than
161
+ * inventing a commit list. See the note above `listCommits`.
162
+ */
163
+ export declare class ValOpsMemory extends ValOps {
164
+ /**
165
+ * The host's own store -- see {@link ValOps.patchesAreLocal}. `true` for the
166
+ * same reason `fs` mode is: nothing is relayed to a content service, so
167
+ * there is no session to verify against one and no group to separate authors
168
+ * in. Where the two differ is not something a route asks about.
169
+ */
170
+ readonly patchesAreLocal = true;
171
+ /**
172
+ * Required, unless the host explicitly takes the boundary itself.
173
+ *
174
+ * `patchesAreLocal` is true here and that is about publishing, not about who
175
+ * may write -- see {@link ValOps.requiresAuth}.
176
+ */
177
+ readonly requiresAuth: boolean;
178
+ private readonly store;
179
+ /**
180
+ * The project's source, keyed WITHOUT a leading slash.
181
+ *
182
+ * Two spellings reach this. A host keys by project-relative path
183
+ * (`src/routes/page.val.ts`) because that is what it built from; Val asks and
184
+ * commits with a leading slash (`/src/routes/page.val.ts`). Normalised on the
185
+ * way in so there is one entry per file — holding both spellings would let a
186
+ * commit update one and leave the other as the stale answer.
187
+ *
188
+ * Not `readonly`: a save replaces the files it rewrote. See
189
+ * {@link adoptPatchedSourceFiles}.
190
+ */
191
+ private sourceFiles;
192
+ private readonly contentUrl;
193
+ constructor(valModules: ValModules, options: ValOpsMemoryOptions);
194
+ onInit(): Promise<void>;
195
+ /**
196
+ * Requests parked in {@link getStat}, waiting for something to happen.
197
+ *
198
+ * See there for why this exists. Resolved and emptied by
199
+ * {@link announceChange}; never rejected, because a waiter that gives up does
200
+ * so on its own timeout.
201
+ */
202
+ private statWaiters;
203
+ /**
204
+ * Writes so far. Sampled before reading, compared after registering.
205
+ *
206
+ * The registration is not atomic with the read above it: a patch written
207
+ * between `currentStat()` and `statWaiters.push` was announced to a list this
208
+ * waiter was not yet on, so the request slept the full interval with a change
209
+ * already sitting there. Comparing the count closes that window without a
210
+ * lock.
211
+ */
212
+ private changeCount;
213
+ /** Wake every parked `getStat`. Called by this instance's own writes. */
214
+ private announceChange;
215
+ private currentStat;
216
+ getStat(params: {
217
+ baseSha: BaseSha;
218
+ schemaSha: SchemaSha;
219
+ patches?: PatchId[];
220
+ } | null): Promise<{
221
+ type: "request-again" | "no-change" | "did-change";
222
+ baseSha: BaseSha;
223
+ schemaSha: SchemaSha;
224
+ sourcesSha: SourcesSha;
225
+ patches: PatchId[];
226
+ }>;
227
+ /** Resolves on the next write here, or when the poll interval runs out. */
228
+ private parkUntilChange;
229
+ fetchPatches<ExcludePatchOps extends boolean>(filters: {
230
+ patchIds?: PatchId[];
231
+ excludePatchOps: ExcludePatchOps;
232
+ }): Promise<ExcludePatchOps extends true ? OrderedPatchesMetadata : OrderedPatches>;
233
+ protected saveSourceFilePatch(path: ModuleFilePath, patch: Patch, patchId: PatchId, _parentRef: ParentRef | null, authorId: AuthorId | null, _sessionId: string | null, _patchGroup?: PatchGroupMembership): Promise<SaveSourceFilePatchResult>;
234
+ /** One spelling for a path, whichever the caller used. See sourceFiles. */
235
+ private static key;
236
+ /**
237
+ * A save has rewritten these files; they are the committed source now.
238
+ *
239
+ * Without this every save after the first re-reads the source as it was when
240
+ * this object was built, applies only its own patches to that, and parks a
241
+ * file that reverts everything saved before it -- with no error, because
242
+ * applying the patch to the ORIGINAL text succeeds. The Studio auto-saves, so
243
+ * that is not an edge case: it is most of a session's work.
244
+ *
245
+ * `fs` mode gets this for free -- `saveOrUploadFiles` writes the disk that
246
+ * `getSourceFile` reads. There is no disk here, so it is written down.
247
+ */
248
+ protected adoptPatchedSourceFiles(files: Record<string, string | null>): void;
249
+ protected getSourceFile(path: string): Promise<WithGenericError<{
250
+ data: string;
251
+ }>>;
252
+ deletePatches(patchIds: PatchId[]): Promise<{
253
+ deleted: PatchId[];
254
+ errors?: undefined;
255
+ error?: undefined;
256
+ }>;
257
+ /**
258
+ * Push this commit's pending binary files to Val's content host.
259
+ *
260
+ * The half of publishing that `commitPrepared` cannot do. Val's remote files
261
+ * upload at PUBLISH, not when the image is added: until then the bytes are a
262
+ * pending change like any other, held by {@link ValPatchStore}. So a publish
263
+ * has to walk the descriptors and push each one before the source that
264
+ * references it goes live -- otherwise the new build ships a URL that 404s.
265
+ *
266
+ * `ValOpsFS.saveOrUploadFiles` does the same loop, alongside two things this
267
+ * has no use for: copying LOCAL binaries into a working tree, and writing the
268
+ * source files (which is `commitPrepared` here). Kept separate rather than
269
+ * shared, because the shapes only look alike.
270
+ *
271
+ * Errors are collected rather than thrown. One image that will not upload
272
+ * should name itself and leave the rest of the publish decidable, rather than
273
+ * failing a save that has already applied its patches.
274
+ */
275
+ uploadRemoteFiles(preparedCommit: Pick<PreparedCommit, "patchedBinaryFilesDescriptors">, auth: {
276
+ apiKey: string;
277
+ } | {
278
+ pat: string;
279
+ }): Promise<{
280
+ uploaded: string[];
281
+ errors: Record<string, GenericErrorMessage>;
282
+ }>;
283
+ private remoteOnly;
284
+ saveBase64EncodedBinaryFileFromPatch(filePath: string, _parentRef: ParentRef, patchId: PatchId, data: string | null, _type: "file" | "image", metadata: MetadataOfType<"file" | "image"> | undefined): Promise<WithGenericError<{
285
+ patchId: PatchId;
286
+ filePath: string;
287
+ }>>;
288
+ getBase64EncodedBinaryFileFromPatch(filePath: string, patchId: PatchId, _remote?: boolean): Promise<Buffer | null>;
289
+ protected getBase64EncodedBinaryFileMetadataFromPatch<T extends "file" | "image">(filePath: string, type: T, patchId: PatchId, _remote?: boolean): Promise<OpsMetadata<T>>;
290
+ getBinaryFile(_filePathOrRef: string): Promise<Buffer | null>;
291
+ protected getBinaryFileMetadata<T extends "file" | "image">(_filePath: string, _type: T): Promise<OpsMetadata<T>>;
292
+ listCommits(): Promise<result.Result<CommitPage, HistoryError>>;
293
+ getCommitPatches(): Promise<result.Result<{
294
+ commit: HistoricalCommit;
295
+ patches: CommitPatch[];
296
+ }, HistoryError>>;
297
+ getCommitModules(): Promise<result.Result<{
298
+ modules: StoredModuleVersion[];
299
+ complete: boolean;
300
+ }, HistoryError>>;
301
+ getCommitAffectedFiles(): Promise<result.Result<AffectedFile[], HistoryError>>;
302
+ getFileAtCommit(): Promise<result.Result<Buffer, HistoryError>>;
303
+ gitPathOfModule(_moduleFilePath: ModuleFilePath): result.Result<string, HistoryError>;
304
+ }
305
+ /** Kept exported so a host can name the error shape it may get back. */
306
+ export type ValOpsMemoryError = GenericErrorMessage;
@@ -7,6 +7,8 @@ type Versions = {
7
7
  next?: string;
8
8
  };
9
9
  };
10
+ import type { ValPatchStore } from "./ValOpsMemory.js";
11
+ import type { CommitContext, CommitResult } from "./ValServer.js";
10
12
  export type ValApiOptions = ValServerOverrides & ValConfig & Versions;
11
13
  type ValServerOverrides = Partial<{
12
14
  /**
@@ -46,6 +48,41 @@ type ValServerOverrides = Partial<{
46
48
  * If both is missing, it will default to "local".
47
49
  */
48
50
  mode: "proxy" | "local";
51
+ /**
52
+ * The project's source, by path -- and, by being present, the choice of an
53
+ * in-memory store over the local filesystem.
54
+ *
55
+ * EXPERIMENTAL. For a host that HOLDS the project's source rather than having
56
+ * it on a disk: it hands it over here, patches live in `patchStore`, and a
57
+ * publish is whatever `commitPrepared` does with the files. See
58
+ * `ValOpsMemory`.
59
+ *
60
+ * Selected by presence rather than by a `mode` value because unlike "local"
61
+ * and "proxy" this one cannot be inferred from the environment -- there is
62
+ * nothing to infer it FROM, and a mode that can be turned on without
63
+ * supplying the source would be a server with no content in it.
64
+ *
65
+ * An environment that has no disk can still say it EXPECTS this, by setting
66
+ * `VAL_MODE=memory`. That does not select the mode; it makes forgetting to
67
+ * pass the source an error here rather than an `EPERM` from `fs` mode two
68
+ * layers down.
69
+ */
70
+ sourceFiles: Record<string, string>;
71
+ /**
72
+ * Serve memory mode without authenticating any request. Off by default.
73
+ *
74
+ * Set this only when the host authorises every request before it reaches
75
+ * Val. Without it, memory mode requires a verified session like `http` mode
76
+ * does -- unlike `fs` mode, this one runs deployed, so an unauthenticated
77
+ * server is one where anyone who can reach the port can create patches and
78
+ * trigger a publish.
79
+ */
80
+ unsafelyAllowUnauthenticated?: boolean;
81
+ /**
82
+ * Where pending patches live, with {@link sourceFiles}. Defaults to memory,
83
+ * which is not durable -- see `ValPatchStore`.
84
+ */
85
+ patchStore: ValPatchStore;
49
86
  /**
50
87
  * Current git commit.
51
88
  *
@@ -113,7 +150,32 @@ type ValServerOverrides = Partial<{
113
150
  */
114
151
  disableCache?: boolean;
115
152
  }>;
116
- export declare function createValServer(valModules: ValModules, route: string, opts: ValApiOptions, config: ValConfig, callbacks: ValServerCallbacks, formatter?: (code: string, filePath: string) => string | Promise<string>): Promise<ValServer>;
153
+ export declare function createValServer(valModules: ValModules, route: string, opts: ValApiOptions, config: ValConfig, callbacks: ValServerCallbacks, formatter?: (code: string, filePath: string) => string | Promise<string>,
154
+ /**
155
+ * Called after a save has applied its patches. EXPERIMENTAL — see
156
+ * `ValServerOptions.commitPrepared`.
157
+ */
158
+ commitPrepared?: (commit: {
159
+ patchedSourceFiles: Record<string, string | null>;
160
+ }) => Promise<void>,
161
+ /**
162
+ * What a publish does in http mode. EXPERIMENTAL — see
163
+ * `ValServerOptions.publishOverride`.
164
+ */
165
+ publishOverride?: (context: CommitContext) => Promise<CommitResult>): Promise<ValServer>;
166
+ /**
167
+ * `fs` and `path` are imported INSIDE this function, not at the top of the file.
168
+ *
169
+ * This is the only thing in this module that touches either, and it is a local
170
+ * development convenience: scanning upwards for a `.git` to guess the commit and
171
+ * branch. A static import put `fs` in the module graph of everything reaching
172
+ * `createValApiRouter` -- which is every server integration, including ones that
173
+ * run where there is no filesystem. Workerd provides no `fs`, so such a build
174
+ * could not be bundled at all without stubbing it.
175
+ *
176
+ * The `await import` costs nothing here: the only caller is the CLI, on a
177
+ * machine that has both.
178
+ */
117
179
  export declare function safeReadGit(cwd: string): Promise<{
118
180
  commit?: string;
119
181
  branch?: string;