@valbuild/server 0.128.0 → 0.130.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 +105 -0
- package/dist/declarations/src/ValOps.d.ts +50 -1
- package/dist/declarations/src/ValOpsFS.d.ts +7 -0
- package/dist/declarations/src/ValOpsHttp.d.ts +4 -0
- package/dist/declarations/src/ValOpsMemory.d.ts +306 -0
- package/dist/declarations/src/ValRouter.d.ts +58 -1
- package/dist/declarations/src/ValServer.d.ts +88 -3
- package/dist/declarations/src/getSettings.d.ts +3 -1
- package/dist/declarations/src/index.d.ts +2 -0
- package/dist/declarations/src/valServerConfig.d.ts +3 -2
- package/dist/valbuild-server.cjs.dev.js +1026 -73
- package/dist/valbuild-server.cjs.prod.js +1026 -73
- package/dist/valbuild-server.esm.js +1024 -73
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
1
|
# @valbuild/server
|
|
2
2
|
|
|
3
|
+
## 0.130.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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
|
|
8
|
+
|
|
9
|
+
EXPERIMENTAL. `fs` mode assumes a working tree it can watch and write; `http`
|
|
10
|
+
mode assumes Val's content service owns the patch chain and that a commit is a
|
|
11
|
+
git commit. A host that builds and publishes its own output is neither: it holds
|
|
12
|
+
the source already, it has nowhere to watch, and its "commit" is a new build.
|
|
13
|
+
|
|
14
|
+
Forcing such a host into `fs` mode cost three things, all now fixed: `/stat`
|
|
15
|
+
long-polled against watchers that could never fire, burning CPU for the whole
|
|
16
|
+
hold to learn nothing;
|
|
17
|
+
`/api/val/enable` 500'd; and every read of a `.val.ts` went through a shimmed
|
|
18
|
+
filesystem when the host could simply hand the source over.
|
|
19
|
+
|
|
20
|
+
`ValOpsMemory` takes the source as `sourceFiles`, refuses the local binary
|
|
21
|
+
members by name — this configuration uses Val's remote files — and answers the
|
|
22
|
+
history members with the same closed `not-supported-in-fs-mode` error `ValOpsFS`
|
|
23
|
+
gives, so the History UI degrades the way it already knows how rather than
|
|
24
|
+
inventing a commit list.
|
|
25
|
+
|
|
26
|
+
`getStat` still long-polls -- the hold is what paces the client, and an earlier
|
|
27
|
+
version that answered immediately turned a 20-second poll into a request every
|
|
28
|
+
6ms -- but it parks on a SIGNAL rather than a timer. This mode owns its store,
|
|
29
|
+
so it is told when something changes: no timers while parked, and a patch
|
|
30
|
+
written by another tab is seen at once rather than up to 250ms later.
|
|
31
|
+
|
|
32
|
+
Two seams come with it. `commitPrepared` lets a host take what a save produced
|
|
33
|
+
instead of a git commit, and `publishOverride` lets a publish be something other
|
|
34
|
+
than a push. Both are opt-in; an app that sets neither behaves exactly as before.
|
|
35
|
+
|
|
36
|
+
The in-memory patch store is explicitly **not durable**. It is behind
|
|
37
|
+
`ValPatchStore`, so a durable implementation is a swap rather than a rewrite,
|
|
38
|
+
but as shipped a restart loses unpublished patches.
|
|
39
|
+
|
|
40
|
+
**Memory mode authenticates.** `ValOps` gained `requiresAuth` alongside
|
|
41
|
+
`patchesAreLocal`, because one flag was answering two questions: whether a store
|
|
42
|
+
auto-saves or publishes (behaviour, reported as `mode` and keyed off by the UI),
|
|
43
|
+
and whether an unauthenticated request may write (security). With two
|
|
44
|
+
implementations the answers coincided — `fs` is a developer's own machine where
|
|
45
|
+
no credential exists, `http` is remote — so `getAuth` was written against
|
|
46
|
+
`patchesAreLocal` and returned anonymous _success_ for a missing cookie, an
|
|
47
|
+
invalid JWT, an unparseable payload, or no configured secret.
|
|
48
|
+
|
|
49
|
+
Memory mode splits them: its store is local, and it runs deployed. It therefore
|
|
50
|
+
requires a verified session, like `http` mode. A host that authorises requests
|
|
51
|
+
before Val sees them can opt out with `unsafelyAllowUnauthenticated`, which is
|
|
52
|
+
spelled that way on purpose and warns at startup. `fs` mode is unchanged.
|
|
53
|
+
|
|
54
|
+
Val's own MCP endpoint refuses memory mode outright. It has the same absence fs
|
|
55
|
+
mode has — no credential, no backend, every permission check on the far side of
|
|
56
|
+
one — and unlike fs mode it is meant to run deployed, so the existing
|
|
57
|
+
"development only" and loopback guards refuse nothing. A host in this mode owns
|
|
58
|
+
its own trust boundary and can offer the tools through it.
|
|
59
|
+
|
|
60
|
+
Internally, the routes' `instanceof ValOpsFS` checks meant "is this a local
|
|
61
|
+
store" — correct with two implementations and silently wrong with three. They are
|
|
62
|
+
now `ValOps.patchesAreLocal` at all 17 policy sites.
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- [#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
|
|
67
|
+
|
|
68
|
+
A PAT is read from a file in the _server's_ working directory, and only local
|
|
69
|
+
`fs` mode has one. `resolveRemoteFileAuth` knew that; two things upstream did not.
|
|
70
|
+
|
|
71
|
+
`RemoteFilesErrorDialog` was unconditional. Whatever went wrong with remote files,
|
|
72
|
+
it said "Personal access token file required" and told the reader to run a command
|
|
73
|
+
in their project root — for a server with no working directory, a directory that
|
|
74
|
+
does not exist, to produce a file it could not read. The reason was already on the
|
|
75
|
+
error object and simply never looked at. The dialog now shows only for the two
|
|
76
|
+
reasons a PAT can actually fix, and everything else gets its own message.
|
|
77
|
+
|
|
78
|
+
`resolveRemoteFileAuth` also answered `project-not-configured` for a non-fs mode
|
|
79
|
+
with no api key, which is wrong twice: the project may be configured perfectly
|
|
80
|
+
well, and it is the credential that is absent. It answers `api-key-missing` now,
|
|
81
|
+
already in the wire contract, and that message no longer says "production mode",
|
|
82
|
+
because every server that is not local dev gives it.
|
|
83
|
+
|
|
84
|
+
- [#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
|
|
85
|
+
|
|
86
|
+
`fs` and `path` were imported at module scope for `safeReadGit`, a local
|
|
87
|
+
development convenience that scans upwards for a `.git` to guess the commit and
|
|
88
|
+
branch, and whose only caller is the CLI. A static import put `fs` in the module
|
|
89
|
+
graph of everything reaching `createValApiRouter` — which is every server
|
|
90
|
+
integration, including ones that run where there is no filesystem at all.
|
|
91
|
+
|
|
92
|
+
Behaviour is unchanged where there is a filesystem.
|
|
93
|
+
|
|
94
|
+
- 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)]:
|
|
95
|
+
- @valbuild/ui@0.130.0
|
|
96
|
+
- @valbuild/core@0.130.0
|
|
97
|
+
- @valbuild/shared@0.130.0
|
|
98
|
+
|
|
99
|
+
## 0.129.0
|
|
100
|
+
|
|
101
|
+
### Patch Changes
|
|
102
|
+
|
|
103
|
+
- Updated dependencies [[`e20f6fb`](https://github.com/valbuild/val/commit/e20f6fbcc215c310eef49a44c1a592c1e2081613), [`7d34ecc`](https://github.com/valbuild/val/commit/7d34ecce787a0709025ae7b4764cb6c3ad1f766b), [`0c351c4`](https://github.com/valbuild/val/commit/0c351c4f97ae7f09772eab0e856ae69821b803b7), [`41a0d76`](https://github.com/valbuild/val/commit/41a0d76636a2dce3f1e506d93170d97e46041d98), [`9e0ebb0`](https://github.com/valbuild/val/commit/9e0ebb00430f05ef92dff031309f73b8075a7d99)]:
|
|
104
|
+
- @valbuild/ui@0.129.0
|
|
105
|
+
- @valbuild/core@0.129.0
|
|
106
|
+
- @valbuild/shared@0.129.0
|
|
107
|
+
|
|
3
108
|
## 0.128.0
|
|
4
109
|
|
|
5
110
|
### 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,36 @@ 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
|
+
sourceFiles: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Serve memory mode without authenticating any request. Off by default.
|
|
68
|
+
*
|
|
69
|
+
* Set this only when the host authorises every request before it reaches
|
|
70
|
+
* Val. Without it, memory mode requires a verified session like `http` mode
|
|
71
|
+
* does -- unlike `fs` mode, this one runs deployed, so an unauthenticated
|
|
72
|
+
* server is one where anyone who can reach the port can create patches and
|
|
73
|
+
* trigger a publish.
|
|
74
|
+
*/
|
|
75
|
+
unsafelyAllowUnauthenticated?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Where pending patches live, with {@link sourceFiles}. Defaults to memory,
|
|
78
|
+
* which is not durable -- see `ValPatchStore`.
|
|
79
|
+
*/
|
|
80
|
+
patchStore: ValPatchStore;
|
|
49
81
|
/**
|
|
50
82
|
* Current git commit.
|
|
51
83
|
*
|
|
@@ -113,7 +145,32 @@ type ValServerOverrides = Partial<{
|
|
|
113
145
|
*/
|
|
114
146
|
disableCache?: boolean;
|
|
115
147
|
}>;
|
|
116
|
-
export declare function createValServer(valModules: ValModules, route: string, opts: ValApiOptions, config: ValConfig, callbacks: ValServerCallbacks, formatter?: (code: string, filePath: string) => string | Promise<string
|
|
148
|
+
export declare function createValServer(valModules: ValModules, route: string, opts: ValApiOptions, config: ValConfig, callbacks: ValServerCallbacks, formatter?: (code: string, filePath: string) => string | Promise<string>,
|
|
149
|
+
/**
|
|
150
|
+
* Called after a save has applied its patches. EXPERIMENTAL — see
|
|
151
|
+
* `ValServerOptions.commitPrepared`.
|
|
152
|
+
*/
|
|
153
|
+
commitPrepared?: (commit: {
|
|
154
|
+
patchedSourceFiles: Record<string, string | null>;
|
|
155
|
+
}) => Promise<void>,
|
|
156
|
+
/**
|
|
157
|
+
* What a publish does in http mode. EXPERIMENTAL — see
|
|
158
|
+
* `ValServerOptions.publishOverride`.
|
|
159
|
+
*/
|
|
160
|
+
publishOverride?: (context: CommitContext) => Promise<CommitResult>): Promise<ValServer>;
|
|
161
|
+
/**
|
|
162
|
+
* `fs` and `path` are imported INSIDE this function, not at the top of the file.
|
|
163
|
+
*
|
|
164
|
+
* This is the only thing in this module that touches either, and it is a local
|
|
165
|
+
* development convenience: scanning upwards for a `.git` to guess the commit and
|
|
166
|
+
* branch. A static import put `fs` in the module graph of everything reaching
|
|
167
|
+
* `createValApiRouter` -- which is every server integration, including ones that
|
|
168
|
+
* run where there is no filesystem. Workerd provides no `fs`, so such a build
|
|
169
|
+
* could not be bundled at all without stubbing it.
|
|
170
|
+
*
|
|
171
|
+
* The `await import` costs nothing here: the only caller is the CLI, on a
|
|
172
|
+
* machine that has both.
|
|
173
|
+
*/
|
|
117
174
|
export declare function safeReadGit(cwd: string): Promise<{
|
|
118
175
|
commit?: string;
|
|
119
176
|
branch?: string;
|