@valbuild/server 0.123.3 → 0.125.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 +56 -0
- package/dist/declarations/src/ValOps.d.ts +11 -0
- package/dist/declarations/src/ValOpsFS.d.ts +1 -0
- package/dist/declarations/src/ValOpsHttp.d.ts +9 -0
- package/dist/valbuild-server.cjs.dev.js +213 -5
- package/dist/valbuild-server.cjs.prod.js +213 -5
- package/dist/valbuild-server.esm.js +213 -5
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# @valbuild/server
|
|
2
2
|
|
|
3
|
+
## 0.125.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`c390397`](https://github.com/valbuild/val/commit/c390397cb5e203eb1bedae3a6fab15726e850b90)]:
|
|
8
|
+
- @valbuild/core@0.125.0
|
|
9
|
+
- @valbuild/shared@0.125.0
|
|
10
|
+
- @valbuild/ui@0.125.0
|
|
11
|
+
|
|
12
|
+
## 0.124.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- [#639](https://github.com/valbuild/val/pull/639) [`ad7fff4`](https://github.com/valbuild/val/commit/ad7fff4cc8cea98c506cf7ff9b4e8d6e5ffa4055) Thanks [@freekh](https://github.com/freekh)! - Show `.jsonValues()` entries in the history pane
|
|
17
|
+
|
|
18
|
+
A `.jsonValues()` record keeps each entry's content in its own `*.val.json`
|
|
19
|
+
file; the module's own content is just markers pointing at them. The history
|
|
20
|
+
pane had no way to fetch those files for a past commit, so every entry rendered
|
|
21
|
+
as an **empty field** — which reads as "the author left this blank", about
|
|
22
|
+
content that was simply stored somewhere else.
|
|
23
|
+
|
|
24
|
+
Entries now load in the history pane the same way they load in the Studio: one
|
|
25
|
+
at a time, when you open one. A commit with a thousand support pages costs
|
|
26
|
+
nothing until you look at one of them.
|
|
27
|
+
|
|
28
|
+
An entry that cannot be read says so instead of rendering blank — including the
|
|
29
|
+
case where the key did not exist yet at that commit, which is a real answer
|
|
30
|
+
rather than an error.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- [#639](https://github.com/valbuild/val/pull/639) [`ad7fff4`](https://github.com/valbuild/val/commit/ad7fff4cc8cea98c506cf7ff9b4e8d6e5ffa4055) Thanks [@freekh](https://github.com/freekh)! - Require a session to read a file from history
|
|
35
|
+
|
|
36
|
+
`GET /api/val/history/files` served a file at a given commit to anyone who
|
|
37
|
+
asked. It followed the reasoning of `/api/val/files`, which is deliberately
|
|
38
|
+
open — and neither half of that reasoning applies to it:
|
|
39
|
+
|
|
40
|
+
- `/files` stands on `patch_id` being an unguessable UUID. A **commit sha is
|
|
41
|
+
published** — `git log`, the GitHub UI, every pull request — so it is no
|
|
42
|
+
substitute for a credential.
|
|
43
|
+
- `/files` also _cannot_ require auth: draft images are fetched by the app's own
|
|
44
|
+
backend during Next image optimisation, with no cookies to send. Nothing
|
|
45
|
+
fetches history files server-side; both callers are the Studio, in a browser
|
|
46
|
+
that already holds the session.
|
|
47
|
+
|
|
48
|
+
So history files now require a session. `/files` is unchanged, and the reason
|
|
49
|
+
the two differ is written down in `architecture/media.md` so it is not "fixed"
|
|
50
|
+
in either direction later.
|
|
51
|
+
|
|
52
|
+
No action needed: the Studio sends its session cookie automatically.
|
|
53
|
+
|
|
54
|
+
- Updated dependencies [[`5674237`](https://github.com/valbuild/val/commit/56742371a75b4fdcbff8b1afccff8fcc1ebf8078), [`ad7fff4`](https://github.com/valbuild/val/commit/ad7fff4cc8cea98c506cf7ff9b4e8d6e5ffa4055), [`ad7fff4`](https://github.com/valbuild/val/commit/ad7fff4cc8cea98c506cf7ff9b4e8d6e5ffa4055), [`aa89fc2`](https://github.com/valbuild/val/commit/aa89fc26de7028b3c5a1666b99afc03b39e92769)]:
|
|
55
|
+
- @valbuild/ui@0.124.0
|
|
56
|
+
- @valbuild/shared@0.124.0
|
|
57
|
+
- @valbuild/core@0.124.0
|
|
58
|
+
|
|
3
59
|
## 0.123.3
|
|
4
60
|
|
|
5
61
|
### Patch Changes
|
|
@@ -486,6 +486,17 @@ export declare abstract class ValOps {
|
|
|
486
486
|
abstract getCommitAffectedFiles(commitSha: string): Promise<result.Result<AffectedFile[], HistoryError>>;
|
|
487
487
|
/** One file's bytes as they were at one commit. */
|
|
488
488
|
abstract getFileAtCommit(commitSha: string, filePath: string, remote: boolean): Promise<result.Result<Buffer, HistoryError>>;
|
|
489
|
+
/**
|
|
490
|
+
* Where a module lives in the REPOSITORY, as `getFileAtCommit` wants it.
|
|
491
|
+
*
|
|
492
|
+
* A `ModuleFilePath` is project-relative (`/app/page.val.ts`); a git path is
|
|
493
|
+
* repository-relative and carries the project root in front of it
|
|
494
|
+
* (`examples/next/app/page.val.ts`). Only the ops know that root, which is
|
|
495
|
+
* why this is here rather than computed by the history functions - and why a
|
|
496
|
+
* history function that needs to read a module's own file at a commit has to
|
|
497
|
+
* ask instead of concatenating.
|
|
498
|
+
*/
|
|
499
|
+
abstract gitPathOfModule(moduleFilePath: ModuleFilePath): result.Result<string, HistoryError>;
|
|
489
500
|
}
|
|
490
501
|
export type WithGenericError<T extends Record<string, unknown>> = (T & {
|
|
491
502
|
error?: undefined;
|
|
@@ -172,4 +172,5 @@ export declare class ValOpsFS extends ValOps {
|
|
|
172
172
|
}, HistoryError>>;
|
|
173
173
|
getCommitAffectedFiles(): Promise<result.Result<AffectedFile[], HistoryError>>;
|
|
174
174
|
getFileAtCommit(): Promise<result.Result<Buffer, HistoryError>>;
|
|
175
|
+
gitPathOfModule(_moduleFilePath: ModuleFilePath): result.Result<string, HistoryError>;
|
|
175
176
|
}
|
|
@@ -310,6 +310,15 @@ export declare class ValOpsHttp extends ValOps {
|
|
|
310
310
|
complete: boolean;
|
|
311
311
|
}, HistoryError>>;
|
|
312
312
|
getCommitAffectedFiles(commitSha: string): Promise<result.Result<AffectedFile[], HistoryError>>;
|
|
313
|
+
/**
|
|
314
|
+
* `root` in front, and never a doubled or missing slash.
|
|
315
|
+
*
|
|
316
|
+
* `root` is "" for a project at the repository root and something like
|
|
317
|
+
* `examples/next` otherwise; a ModuleFilePath always starts with "/". Joining
|
|
318
|
+
* them by hand at each call site is how one of these ends up with "//" in the
|
|
319
|
+
* middle, which GitHub answers with a 404 that reads like a missing file.
|
|
320
|
+
*/
|
|
321
|
+
gitPathOfModule(moduleFilePath: ModuleFilePath): result.Result<string, HistoryError>;
|
|
313
322
|
getFileAtCommit(commitSha: string, filePath: string, remote: boolean): Promise<result.Result<Buffer, HistoryError>>;
|
|
314
323
|
}
|
|
315
324
|
export {};
|
|
@@ -4118,6 +4118,17 @@ class ValOps {
|
|
|
4118
4118
|
|
|
4119
4119
|
/** One file's bytes as they were at one commit. */
|
|
4120
4120
|
|
|
4121
|
+
/**
|
|
4122
|
+
* Where a module lives in the REPOSITORY, as `getFileAtCommit` wants it.
|
|
4123
|
+
*
|
|
4124
|
+
* A `ModuleFilePath` is project-relative (`/app/page.val.ts`); a git path is
|
|
4125
|
+
* repository-relative and carries the project root in front of it
|
|
4126
|
+
* (`examples/next/app/page.val.ts`). Only the ops know that root, which is
|
|
4127
|
+
* why this is here rather than computed by the history functions - and why a
|
|
4128
|
+
* history function that needs to read a module's own file at a commit has to
|
|
4129
|
+
* ask instead of concatenating.
|
|
4130
|
+
*/
|
|
4131
|
+
|
|
4121
4132
|
// #endregion history
|
|
4122
4133
|
}
|
|
4123
4134
|
function isOnlyFileCheckValidationError(validationError) {
|
|
@@ -6763,6 +6774,14 @@ class ValOpsFS extends ValOps {
|
|
|
6763
6774
|
kind: "not-supported-in-fs-mode"
|
|
6764
6775
|
});
|
|
6765
6776
|
}
|
|
6777
|
+
gitPathOfModule(
|
|
6778
|
+
// Unused: fs mode has no repository to be relative to. Named anyway, so
|
|
6779
|
+
// this reads as the same method the base class declares.
|
|
6780
|
+
_moduleFilePath) {
|
|
6781
|
+
return fp.result.err({
|
|
6782
|
+
kind: "not-supported-in-fs-mode"
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6766
6785
|
// #endregion history
|
|
6767
6786
|
}
|
|
6768
6787
|
class FSOpsHost {
|
|
@@ -8554,6 +8573,19 @@ class ValOpsHttp extends ValOps {
|
|
|
8554
8573
|
}
|
|
8555
8574
|
return fp.result.ok(res.value.files);
|
|
8556
8575
|
}
|
|
8576
|
+
|
|
8577
|
+
/**
|
|
8578
|
+
* `root` in front, and never a doubled or missing slash.
|
|
8579
|
+
*
|
|
8580
|
+
* `root` is "" for a project at the repository root and something like
|
|
8581
|
+
* `examples/next` otherwise; a ModuleFilePath always starts with "/". Joining
|
|
8582
|
+
* them by hand at each call site is how one of these ends up with "//" in the
|
|
8583
|
+
* middle, which GitHub answers with a 404 that reads like a missing file.
|
|
8584
|
+
*/
|
|
8585
|
+
gitPathOfModule(moduleFilePath) {
|
|
8586
|
+
const joined = `${this.root}/${moduleFilePath}`.split("/").filter(part => part !== "").join("/");
|
|
8587
|
+
return fp.result.ok(joined);
|
|
8588
|
+
}
|
|
8557
8589
|
async getFileAtCommit(commitSha, filePath, remote) {
|
|
8558
8590
|
const params = new URLSearchParams({
|
|
8559
8591
|
path: filePath
|
|
@@ -9013,6 +9045,129 @@ async function getModuleAtCommit(ops, commitSha, moduleFilePath) {
|
|
|
9013
9045
|
});
|
|
9014
9046
|
}
|
|
9015
9047
|
|
|
9048
|
+
/**
|
|
9049
|
+
* One `.jsonValues()` entry's content, as it was at a commit.
|
|
9050
|
+
*
|
|
9051
|
+
* The history pane renders a commit through the real field components, and a
|
|
9052
|
+
* `.jsonValues()` record's source is only `{_type:"json"}` markers - the
|
|
9053
|
+
* content lives in a separate `*.val.json` per entry, fetched on demand. The
|
|
9054
|
+
* live Studio fetches it from `GET /json`; the commit pane had nothing to
|
|
9055
|
+
* fetch from, so every entry stayed a marker and rendered as though the field
|
|
9056
|
+
* were empty. Which is worse than an error: an empty value is a claim, and it
|
|
9057
|
+
* was a false one.
|
|
9058
|
+
*
|
|
9059
|
+
* ## Why this needs the module's own `.val.ts`
|
|
9060
|
+
*
|
|
9061
|
+
* There is no derivable relationship between a record key and its entry file.
|
|
9062
|
+
* From the example app:
|
|
9063
|
+
*
|
|
9064
|
+
* "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
|
|
9065
|
+
* "/support/faq": c.json(() => import("./content/faq.val.json"))
|
|
9066
|
+
*
|
|
9067
|
+
* The key is a route and the path is whatever the author wrote. The only place
|
|
9068
|
+
* the two are related is the `import()` literal inside the `.val.ts`, which is
|
|
9069
|
+
* why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
|
|
9070
|
+
* commit archive stores the module's SOURCE (the markers) and its schema, not
|
|
9071
|
+
* its text - so the text is fetched from git at that commit and parsed here.
|
|
9072
|
+
*
|
|
9073
|
+
* Two round trips per entry, then: the module, and the entry. The module's
|
|
9074
|
+
* parse is worth caching per (commit, module) by the caller; nothing here does,
|
|
9075
|
+
* because a `ValOps` is not a cache and the endpoint above it is the layer that
|
|
9076
|
+
* knows how long a commit stays interesting.
|
|
9077
|
+
*/
|
|
9078
|
+
async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
|
|
9079
|
+
const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
|
|
9080
|
+
if (fp.result.isErr(moduleGitPath)) {
|
|
9081
|
+
return moduleGitPath;
|
|
9082
|
+
}
|
|
9083
|
+
const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
|
|
9084
|
+
if (fp.result.isErr(moduleRes)) {
|
|
9085
|
+
return moduleRes;
|
|
9086
|
+
}
|
|
9087
|
+
const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
|
|
9088
|
+
if (fp.result.isErr(entryPath)) {
|
|
9089
|
+
return entryPath;
|
|
9090
|
+
}
|
|
9091
|
+
// The entry path resolved above is PROJECT-relative, like the module path it
|
|
9092
|
+
// came from; the git path needs the root in front of it, and the ops are what
|
|
9093
|
+
// know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
|
|
9094
|
+
// a `.val.json` is not a module, but "project-relative path -> repo path" is
|
|
9095
|
+
// the same question and one implementation of it is the point.
|
|
9096
|
+
const entryGitPath = ops.gitPathOfModule(entryPath.value);
|
|
9097
|
+
if (fp.result.isErr(entryGitPath)) {
|
|
9098
|
+
return entryGitPath;
|
|
9099
|
+
}
|
|
9100
|
+
const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
|
|
9101
|
+
if (fp.result.isErr(entryRes)) {
|
|
9102
|
+
return entryRes;
|
|
9103
|
+
}
|
|
9104
|
+
try {
|
|
9105
|
+
return fp.result.ok(JSON.parse(entryRes.value.toString("utf-8")));
|
|
9106
|
+
} catch (err) {
|
|
9107
|
+
return fp.result.err({
|
|
9108
|
+
kind: "file-unavailable",
|
|
9109
|
+
gitPath: entryGitPath.value,
|
|
9110
|
+
message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
|
|
9111
|
+
});
|
|
9112
|
+
}
|
|
9113
|
+
}
|
|
9114
|
+
|
|
9115
|
+
/**
|
|
9116
|
+
* The `import()` path a key's entry is behind, from the module's text.
|
|
9117
|
+
*
|
|
9118
|
+
* Exported for its own test: this is the part with a real chance of being
|
|
9119
|
+
* wrong, and it is pure.
|
|
9120
|
+
*/
|
|
9121
|
+
function findEntryImportPath(moduleFilePath,
|
|
9122
|
+
/**
|
|
9123
|
+
* The same module, repository-relative, for error reporting only.
|
|
9124
|
+
*
|
|
9125
|
+
* A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
|
|
9126
|
+
* other history helper reports, and what a reader can paste into a `git show`
|
|
9127
|
+
* - so reporting the project-relative ModuleFilePath here named a file that
|
|
9128
|
+
* does not exist at that path for any project not rooted at the repo root.
|
|
9129
|
+
*/
|
|
9130
|
+
moduleGitPath, valTsSource, key) {
|
|
9131
|
+
const sourceFile = ts__default["default"].createSourceFile(moduleFilePath, valTsSource, ts__default["default"].ScriptTarget.ES2015, true);
|
|
9132
|
+
let analysis;
|
|
9133
|
+
try {
|
|
9134
|
+
analysis = analyzeValModule(sourceFile);
|
|
9135
|
+
} catch (err) {
|
|
9136
|
+
// `file-unavailable` rather than a kind of its own: the module's TEXT is
|
|
9137
|
+
// the file we could not use, and adding a wire kind for "unparseable"
|
|
9138
|
+
// would be a new case every reader has to handle to say the same thing.
|
|
9139
|
+
return fp.result.err({
|
|
9140
|
+
kind: "file-unavailable",
|
|
9141
|
+
gitPath: moduleGitPath,
|
|
9142
|
+
message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
|
|
9143
|
+
});
|
|
9144
|
+
}
|
|
9145
|
+
if (fp.result.isErr(analysis)) {
|
|
9146
|
+
return fp.result.err({
|
|
9147
|
+
kind: "file-unavailable",
|
|
9148
|
+
gitPath: moduleGitPath,
|
|
9149
|
+
message: "could not read the module at this commit"
|
|
9150
|
+
});
|
|
9151
|
+
}
|
|
9152
|
+
const entries = analyzeJsonValuesEntries(analysis.value.source);
|
|
9153
|
+
const entry = entries.get(key);
|
|
9154
|
+
if (entry === undefined) {
|
|
9155
|
+
/*
|
|
9156
|
+
* Reported, not treated as empty. A key with no entry at this commit means
|
|
9157
|
+
* the entry was added later - so "there is nothing to show for it here" is
|
|
9158
|
+
* the true answer, and rendering an empty field instead would say the
|
|
9159
|
+
* author had left it blank.
|
|
9160
|
+
*/
|
|
9161
|
+
return fp.result.err({
|
|
9162
|
+
kind: "file-unavailable",
|
|
9163
|
+
gitPath: moduleGitPath,
|
|
9164
|
+
message: `'${key}' had no entry at this commit`
|
|
9165
|
+
});
|
|
9166
|
+
}
|
|
9167
|
+
const moduleDir = path__namespace["default"].posix.dirname(moduleFilePath);
|
|
9168
|
+
return fp.result.ok(path__namespace["default"].posix.join(moduleDir, entry.importPath));
|
|
9169
|
+
}
|
|
9170
|
+
|
|
9016
9171
|
const host = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
9017
9172
|
const SettingsSchema = z.z.object({
|
|
9018
9173
|
publicProjectId: z.z.string(),
|
|
@@ -12308,12 +12463,60 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12308
12463
|
};
|
|
12309
12464
|
}
|
|
12310
12465
|
},
|
|
12466
|
+
"/history/json": {
|
|
12467
|
+
GET: async req => {
|
|
12468
|
+
// Authenticated, for the reasons on /history/files below.
|
|
12469
|
+
const auth = getAuth(req.cookies);
|
|
12470
|
+
if (auth.error) {
|
|
12471
|
+
return {
|
|
12472
|
+
status: 401,
|
|
12473
|
+
json: {
|
|
12474
|
+
message: auth.error
|
|
12475
|
+
}
|
|
12476
|
+
};
|
|
12477
|
+
}
|
|
12478
|
+
const res = await getJsonEntryAtCommit(serverOps, req.query.commit_sha, req.query.path, req.query.key);
|
|
12479
|
+
if (fp.result.isErr(res)) {
|
|
12480
|
+
return historyErrorResponse(res.error);
|
|
12481
|
+
}
|
|
12482
|
+
return {
|
|
12483
|
+
status: 200,
|
|
12484
|
+
json: {
|
|
12485
|
+
path: req.query.path,
|
|
12486
|
+
key: req.query.key,
|
|
12487
|
+
content: res.value
|
|
12488
|
+
}
|
|
12489
|
+
};
|
|
12490
|
+
}
|
|
12491
|
+
},
|
|
12311
12492
|
"/history/files": {
|
|
12312
12493
|
GET: async req => {
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12494
|
+
/*
|
|
12495
|
+
* Authenticated, unlike /files.
|
|
12496
|
+
*
|
|
12497
|
+
* This used to reason "same as /files" and that was wrong twice over.
|
|
12498
|
+
* /files is open because a `patch_id` is an unguessable UUID standing
|
|
12499
|
+
* in for a credential, and because it HAS to be: a draft image is
|
|
12500
|
+
* fetched by the app's own backend during Next image optimisation,
|
|
12501
|
+
* backend-to-backend, with no cookies to send.
|
|
12502
|
+
*
|
|
12503
|
+
* A commit sha is not a secret - it is in `git log`, in the GitHub UI,
|
|
12504
|
+
* on every PR - so the first argument does not transfer. And nothing
|
|
12505
|
+
* fetches this server-side; both callers are the Studio in a browser
|
|
12506
|
+
* that has the session cookie (the history pane's <img>, and
|
|
12507
|
+
* stageRestore's fetch, both same-origin so the cookie goes). So the
|
|
12508
|
+
* second does not either, and there is nothing to trade away by
|
|
12509
|
+
* asking.
|
|
12510
|
+
*/
|
|
12511
|
+
const auth = getAuth(req.cookies);
|
|
12512
|
+
if (auth.error) {
|
|
12513
|
+
return {
|
|
12514
|
+
status: 401,
|
|
12515
|
+
json: {
|
|
12516
|
+
message: auth.error
|
|
12517
|
+
}
|
|
12518
|
+
};
|
|
12519
|
+
}
|
|
12317
12520
|
const res = await serverOps.getFileAtCommit(req.query.commit_sha, req.query.path, req.query.remote === "true");
|
|
12318
12521
|
if (fp.result.isErr(res)) {
|
|
12319
12522
|
const response = historyErrorResponse(res.error);
|
|
@@ -12352,6 +12555,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12352
12555
|
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
|
12353
12556
|
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
12354
12557
|
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
12558
|
+
// Note that /history/files, which looks like the same endpoint, IS
|
|
12559
|
+
// authenticated: its token is a commit sha, which is published, and
|
|
12560
|
+
// nothing fetches it backend-to-backend. Neither half of the argument
|
|
12561
|
+
// above transfers. See architecture/media.md, "Why /files has no auth,
|
|
12562
|
+
// and /history/files does".
|
|
12355
12563
|
let fileBuffer;
|
|
12356
12564
|
let mimeType;
|
|
12357
12565
|
const remote = query.remote === "true";
|
|
@@ -14716,7 +14924,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
14716
14924
|
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
14717
14925
|
}
|
|
14718
14926
|
|
|
14719
|
-
// Gallery (s.images({
|
|
14927
|
+
// Gallery (s.images({ ... }).remote() / s.files({ ... }).remote()) upload.
|
|
14720
14928
|
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
14721
14929
|
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
14722
14930
|
// ref from the key and synthesize the image/file schema from the record's
|
|
@@ -4118,6 +4118,17 @@ class ValOps {
|
|
|
4118
4118
|
|
|
4119
4119
|
/** One file's bytes as they were at one commit. */
|
|
4120
4120
|
|
|
4121
|
+
/**
|
|
4122
|
+
* Where a module lives in the REPOSITORY, as `getFileAtCommit` wants it.
|
|
4123
|
+
*
|
|
4124
|
+
* A `ModuleFilePath` is project-relative (`/app/page.val.ts`); a git path is
|
|
4125
|
+
* repository-relative and carries the project root in front of it
|
|
4126
|
+
* (`examples/next/app/page.val.ts`). Only the ops know that root, which is
|
|
4127
|
+
* why this is here rather than computed by the history functions - and why a
|
|
4128
|
+
* history function that needs to read a module's own file at a commit has to
|
|
4129
|
+
* ask instead of concatenating.
|
|
4130
|
+
*/
|
|
4131
|
+
|
|
4121
4132
|
// #endregion history
|
|
4122
4133
|
}
|
|
4123
4134
|
function isOnlyFileCheckValidationError(validationError) {
|
|
@@ -6763,6 +6774,14 @@ class ValOpsFS extends ValOps {
|
|
|
6763
6774
|
kind: "not-supported-in-fs-mode"
|
|
6764
6775
|
});
|
|
6765
6776
|
}
|
|
6777
|
+
gitPathOfModule(
|
|
6778
|
+
// Unused: fs mode has no repository to be relative to. Named anyway, so
|
|
6779
|
+
// this reads as the same method the base class declares.
|
|
6780
|
+
_moduleFilePath) {
|
|
6781
|
+
return fp.result.err({
|
|
6782
|
+
kind: "not-supported-in-fs-mode"
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6766
6785
|
// #endregion history
|
|
6767
6786
|
}
|
|
6768
6787
|
class FSOpsHost {
|
|
@@ -8554,6 +8573,19 @@ class ValOpsHttp extends ValOps {
|
|
|
8554
8573
|
}
|
|
8555
8574
|
return fp.result.ok(res.value.files);
|
|
8556
8575
|
}
|
|
8576
|
+
|
|
8577
|
+
/**
|
|
8578
|
+
* `root` in front, and never a doubled or missing slash.
|
|
8579
|
+
*
|
|
8580
|
+
* `root` is "" for a project at the repository root and something like
|
|
8581
|
+
* `examples/next` otherwise; a ModuleFilePath always starts with "/". Joining
|
|
8582
|
+
* them by hand at each call site is how one of these ends up with "//" in the
|
|
8583
|
+
* middle, which GitHub answers with a 404 that reads like a missing file.
|
|
8584
|
+
*/
|
|
8585
|
+
gitPathOfModule(moduleFilePath) {
|
|
8586
|
+
const joined = `${this.root}/${moduleFilePath}`.split("/").filter(part => part !== "").join("/");
|
|
8587
|
+
return fp.result.ok(joined);
|
|
8588
|
+
}
|
|
8557
8589
|
async getFileAtCommit(commitSha, filePath, remote) {
|
|
8558
8590
|
const params = new URLSearchParams({
|
|
8559
8591
|
path: filePath
|
|
@@ -9013,6 +9045,129 @@ async function getModuleAtCommit(ops, commitSha, moduleFilePath) {
|
|
|
9013
9045
|
});
|
|
9014
9046
|
}
|
|
9015
9047
|
|
|
9048
|
+
/**
|
|
9049
|
+
* One `.jsonValues()` entry's content, as it was at a commit.
|
|
9050
|
+
*
|
|
9051
|
+
* The history pane renders a commit through the real field components, and a
|
|
9052
|
+
* `.jsonValues()` record's source is only `{_type:"json"}` markers - the
|
|
9053
|
+
* content lives in a separate `*.val.json` per entry, fetched on demand. The
|
|
9054
|
+
* live Studio fetches it from `GET /json`; the commit pane had nothing to
|
|
9055
|
+
* fetch from, so every entry stayed a marker and rendered as though the field
|
|
9056
|
+
* were empty. Which is worse than an error: an empty value is a claim, and it
|
|
9057
|
+
* was a false one.
|
|
9058
|
+
*
|
|
9059
|
+
* ## Why this needs the module's own `.val.ts`
|
|
9060
|
+
*
|
|
9061
|
+
* There is no derivable relationship between a record key and its entry file.
|
|
9062
|
+
* From the example app:
|
|
9063
|
+
*
|
|
9064
|
+
* "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
|
|
9065
|
+
* "/support/faq": c.json(() => import("./content/faq.val.json"))
|
|
9066
|
+
*
|
|
9067
|
+
* The key is a route and the path is whatever the author wrote. The only place
|
|
9068
|
+
* the two are related is the `import()` literal inside the `.val.ts`, which is
|
|
9069
|
+
* why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
|
|
9070
|
+
* commit archive stores the module's SOURCE (the markers) and its schema, not
|
|
9071
|
+
* its text - so the text is fetched from git at that commit and parsed here.
|
|
9072
|
+
*
|
|
9073
|
+
* Two round trips per entry, then: the module, and the entry. The module's
|
|
9074
|
+
* parse is worth caching per (commit, module) by the caller; nothing here does,
|
|
9075
|
+
* because a `ValOps` is not a cache and the endpoint above it is the layer that
|
|
9076
|
+
* knows how long a commit stays interesting.
|
|
9077
|
+
*/
|
|
9078
|
+
async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
|
|
9079
|
+
const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
|
|
9080
|
+
if (fp.result.isErr(moduleGitPath)) {
|
|
9081
|
+
return moduleGitPath;
|
|
9082
|
+
}
|
|
9083
|
+
const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
|
|
9084
|
+
if (fp.result.isErr(moduleRes)) {
|
|
9085
|
+
return moduleRes;
|
|
9086
|
+
}
|
|
9087
|
+
const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
|
|
9088
|
+
if (fp.result.isErr(entryPath)) {
|
|
9089
|
+
return entryPath;
|
|
9090
|
+
}
|
|
9091
|
+
// The entry path resolved above is PROJECT-relative, like the module path it
|
|
9092
|
+
// came from; the git path needs the root in front of it, and the ops are what
|
|
9093
|
+
// know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
|
|
9094
|
+
// a `.val.json` is not a module, but "project-relative path -> repo path" is
|
|
9095
|
+
// the same question and one implementation of it is the point.
|
|
9096
|
+
const entryGitPath = ops.gitPathOfModule(entryPath.value);
|
|
9097
|
+
if (fp.result.isErr(entryGitPath)) {
|
|
9098
|
+
return entryGitPath;
|
|
9099
|
+
}
|
|
9100
|
+
const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
|
|
9101
|
+
if (fp.result.isErr(entryRes)) {
|
|
9102
|
+
return entryRes;
|
|
9103
|
+
}
|
|
9104
|
+
try {
|
|
9105
|
+
return fp.result.ok(JSON.parse(entryRes.value.toString("utf-8")));
|
|
9106
|
+
} catch (err) {
|
|
9107
|
+
return fp.result.err({
|
|
9108
|
+
kind: "file-unavailable",
|
|
9109
|
+
gitPath: entryGitPath.value,
|
|
9110
|
+
message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
|
|
9111
|
+
});
|
|
9112
|
+
}
|
|
9113
|
+
}
|
|
9114
|
+
|
|
9115
|
+
/**
|
|
9116
|
+
* The `import()` path a key's entry is behind, from the module's text.
|
|
9117
|
+
*
|
|
9118
|
+
* Exported for its own test: this is the part with a real chance of being
|
|
9119
|
+
* wrong, and it is pure.
|
|
9120
|
+
*/
|
|
9121
|
+
function findEntryImportPath(moduleFilePath,
|
|
9122
|
+
/**
|
|
9123
|
+
* The same module, repository-relative, for error reporting only.
|
|
9124
|
+
*
|
|
9125
|
+
* A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
|
|
9126
|
+
* other history helper reports, and what a reader can paste into a `git show`
|
|
9127
|
+
* - so reporting the project-relative ModuleFilePath here named a file that
|
|
9128
|
+
* does not exist at that path for any project not rooted at the repo root.
|
|
9129
|
+
*/
|
|
9130
|
+
moduleGitPath, valTsSource, key) {
|
|
9131
|
+
const sourceFile = ts__default["default"].createSourceFile(moduleFilePath, valTsSource, ts__default["default"].ScriptTarget.ES2015, true);
|
|
9132
|
+
let analysis;
|
|
9133
|
+
try {
|
|
9134
|
+
analysis = analyzeValModule(sourceFile);
|
|
9135
|
+
} catch (err) {
|
|
9136
|
+
// `file-unavailable` rather than a kind of its own: the module's TEXT is
|
|
9137
|
+
// the file we could not use, and adding a wire kind for "unparseable"
|
|
9138
|
+
// would be a new case every reader has to handle to say the same thing.
|
|
9139
|
+
return fp.result.err({
|
|
9140
|
+
kind: "file-unavailable",
|
|
9141
|
+
gitPath: moduleGitPath,
|
|
9142
|
+
message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
|
|
9143
|
+
});
|
|
9144
|
+
}
|
|
9145
|
+
if (fp.result.isErr(analysis)) {
|
|
9146
|
+
return fp.result.err({
|
|
9147
|
+
kind: "file-unavailable",
|
|
9148
|
+
gitPath: moduleGitPath,
|
|
9149
|
+
message: "could not read the module at this commit"
|
|
9150
|
+
});
|
|
9151
|
+
}
|
|
9152
|
+
const entries = analyzeJsonValuesEntries(analysis.value.source);
|
|
9153
|
+
const entry = entries.get(key);
|
|
9154
|
+
if (entry === undefined) {
|
|
9155
|
+
/*
|
|
9156
|
+
* Reported, not treated as empty. A key with no entry at this commit means
|
|
9157
|
+
* the entry was added later - so "there is nothing to show for it here" is
|
|
9158
|
+
* the true answer, and rendering an empty field instead would say the
|
|
9159
|
+
* author had left it blank.
|
|
9160
|
+
*/
|
|
9161
|
+
return fp.result.err({
|
|
9162
|
+
kind: "file-unavailable",
|
|
9163
|
+
gitPath: moduleGitPath,
|
|
9164
|
+
message: `'${key}' had no entry at this commit`
|
|
9165
|
+
});
|
|
9166
|
+
}
|
|
9167
|
+
const moduleDir = path__namespace["default"].posix.dirname(moduleFilePath);
|
|
9168
|
+
return fp.result.ok(path__namespace["default"].posix.join(moduleDir, entry.importPath));
|
|
9169
|
+
}
|
|
9170
|
+
|
|
9016
9171
|
const host = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
9017
9172
|
const SettingsSchema = z.z.object({
|
|
9018
9173
|
publicProjectId: z.z.string(),
|
|
@@ -12308,12 +12463,60 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12308
12463
|
};
|
|
12309
12464
|
}
|
|
12310
12465
|
},
|
|
12466
|
+
"/history/json": {
|
|
12467
|
+
GET: async req => {
|
|
12468
|
+
// Authenticated, for the reasons on /history/files below.
|
|
12469
|
+
const auth = getAuth(req.cookies);
|
|
12470
|
+
if (auth.error) {
|
|
12471
|
+
return {
|
|
12472
|
+
status: 401,
|
|
12473
|
+
json: {
|
|
12474
|
+
message: auth.error
|
|
12475
|
+
}
|
|
12476
|
+
};
|
|
12477
|
+
}
|
|
12478
|
+
const res = await getJsonEntryAtCommit(serverOps, req.query.commit_sha, req.query.path, req.query.key);
|
|
12479
|
+
if (fp.result.isErr(res)) {
|
|
12480
|
+
return historyErrorResponse(res.error);
|
|
12481
|
+
}
|
|
12482
|
+
return {
|
|
12483
|
+
status: 200,
|
|
12484
|
+
json: {
|
|
12485
|
+
path: req.query.path,
|
|
12486
|
+
key: req.query.key,
|
|
12487
|
+
content: res.value
|
|
12488
|
+
}
|
|
12489
|
+
};
|
|
12490
|
+
}
|
|
12491
|
+
},
|
|
12311
12492
|
"/history/files": {
|
|
12312
12493
|
GET: async req => {
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12494
|
+
/*
|
|
12495
|
+
* Authenticated, unlike /files.
|
|
12496
|
+
*
|
|
12497
|
+
* This used to reason "same as /files" and that was wrong twice over.
|
|
12498
|
+
* /files is open because a `patch_id` is an unguessable UUID standing
|
|
12499
|
+
* in for a credential, and because it HAS to be: a draft image is
|
|
12500
|
+
* fetched by the app's own backend during Next image optimisation,
|
|
12501
|
+
* backend-to-backend, with no cookies to send.
|
|
12502
|
+
*
|
|
12503
|
+
* A commit sha is not a secret - it is in `git log`, in the GitHub UI,
|
|
12504
|
+
* on every PR - so the first argument does not transfer. And nothing
|
|
12505
|
+
* fetches this server-side; both callers are the Studio in a browser
|
|
12506
|
+
* that has the session cookie (the history pane's <img>, and
|
|
12507
|
+
* stageRestore's fetch, both same-origin so the cookie goes). So the
|
|
12508
|
+
* second does not either, and there is nothing to trade away by
|
|
12509
|
+
* asking.
|
|
12510
|
+
*/
|
|
12511
|
+
const auth = getAuth(req.cookies);
|
|
12512
|
+
if (auth.error) {
|
|
12513
|
+
return {
|
|
12514
|
+
status: 401,
|
|
12515
|
+
json: {
|
|
12516
|
+
message: auth.error
|
|
12517
|
+
}
|
|
12518
|
+
};
|
|
12519
|
+
}
|
|
12317
12520
|
const res = await serverOps.getFileAtCommit(req.query.commit_sha, req.query.path, req.query.remote === "true");
|
|
12318
12521
|
if (fp.result.isErr(res)) {
|
|
12319
12522
|
const response = historyErrorResponse(res.error);
|
|
@@ -12352,6 +12555,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12352
12555
|
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
|
12353
12556
|
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
12354
12557
|
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
12558
|
+
// Note that /history/files, which looks like the same endpoint, IS
|
|
12559
|
+
// authenticated: its token is a commit sha, which is published, and
|
|
12560
|
+
// nothing fetches it backend-to-backend. Neither half of the argument
|
|
12561
|
+
// above transfers. See architecture/media.md, "Why /files has no auth,
|
|
12562
|
+
// and /history/files does".
|
|
12355
12563
|
let fileBuffer;
|
|
12356
12564
|
let mimeType;
|
|
12357
12565
|
const remote = query.remote === "true";
|
|
@@ -14716,7 +14924,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
14716
14924
|
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
14717
14925
|
}
|
|
14718
14926
|
|
|
14719
|
-
// Gallery (s.images({
|
|
14927
|
+
// Gallery (s.images({ ... }).remote() / s.files({ ... }).remote()) upload.
|
|
14720
14928
|
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
14721
14929
|
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
14722
14930
|
// ref from the key and synthesize the image/file schema from the record's
|
|
@@ -4084,6 +4084,17 @@ class ValOps {
|
|
|
4084
4084
|
|
|
4085
4085
|
/** One file's bytes as they were at one commit. */
|
|
4086
4086
|
|
|
4087
|
+
/**
|
|
4088
|
+
* Where a module lives in the REPOSITORY, as `getFileAtCommit` wants it.
|
|
4089
|
+
*
|
|
4090
|
+
* A `ModuleFilePath` is project-relative (`/app/page.val.ts`); a git path is
|
|
4091
|
+
* repository-relative and carries the project root in front of it
|
|
4092
|
+
* (`examples/next/app/page.val.ts`). Only the ops know that root, which is
|
|
4093
|
+
* why this is here rather than computed by the history functions - and why a
|
|
4094
|
+
* history function that needs to read a module's own file at a commit has to
|
|
4095
|
+
* ask instead of concatenating.
|
|
4096
|
+
*/
|
|
4097
|
+
|
|
4087
4098
|
// #endregion history
|
|
4088
4099
|
}
|
|
4089
4100
|
function isOnlyFileCheckValidationError(validationError) {
|
|
@@ -6729,6 +6740,14 @@ class ValOpsFS extends ValOps {
|
|
|
6729
6740
|
kind: "not-supported-in-fs-mode"
|
|
6730
6741
|
});
|
|
6731
6742
|
}
|
|
6743
|
+
gitPathOfModule(
|
|
6744
|
+
// Unused: fs mode has no repository to be relative to. Named anyway, so
|
|
6745
|
+
// this reads as the same method the base class declares.
|
|
6746
|
+
_moduleFilePath) {
|
|
6747
|
+
return result.err({
|
|
6748
|
+
kind: "not-supported-in-fs-mode"
|
|
6749
|
+
});
|
|
6750
|
+
}
|
|
6732
6751
|
// #endregion history
|
|
6733
6752
|
}
|
|
6734
6753
|
class FSOpsHost {
|
|
@@ -8520,6 +8539,19 @@ class ValOpsHttp extends ValOps {
|
|
|
8520
8539
|
}
|
|
8521
8540
|
return result.ok(res.value.files);
|
|
8522
8541
|
}
|
|
8542
|
+
|
|
8543
|
+
/**
|
|
8544
|
+
* `root` in front, and never a doubled or missing slash.
|
|
8545
|
+
*
|
|
8546
|
+
* `root` is "" for a project at the repository root and something like
|
|
8547
|
+
* `examples/next` otherwise; a ModuleFilePath always starts with "/". Joining
|
|
8548
|
+
* them by hand at each call site is how one of these ends up with "//" in the
|
|
8549
|
+
* middle, which GitHub answers with a 404 that reads like a missing file.
|
|
8550
|
+
*/
|
|
8551
|
+
gitPathOfModule(moduleFilePath) {
|
|
8552
|
+
const joined = `${this.root}/${moduleFilePath}`.split("/").filter(part => part !== "").join("/");
|
|
8553
|
+
return result.ok(joined);
|
|
8554
|
+
}
|
|
8523
8555
|
async getFileAtCommit(commitSha, filePath, remote) {
|
|
8524
8556
|
const params = new URLSearchParams({
|
|
8525
8557
|
path: filePath
|
|
@@ -8979,6 +9011,129 @@ async function getModuleAtCommit(ops, commitSha, moduleFilePath) {
|
|
|
8979
9011
|
});
|
|
8980
9012
|
}
|
|
8981
9013
|
|
|
9014
|
+
/**
|
|
9015
|
+
* One `.jsonValues()` entry's content, as it was at a commit.
|
|
9016
|
+
*
|
|
9017
|
+
* The history pane renders a commit through the real field components, and a
|
|
9018
|
+
* `.jsonValues()` record's source is only `{_type:"json"}` markers - the
|
|
9019
|
+
* content lives in a separate `*.val.json` per entry, fetched on demand. The
|
|
9020
|
+
* live Studio fetches it from `GET /json`; the commit pane had nothing to
|
|
9021
|
+
* fetch from, so every entry stayed a marker and rendered as though the field
|
|
9022
|
+
* were empty. Which is worse than an error: an empty value is a claim, and it
|
|
9023
|
+
* was a false one.
|
|
9024
|
+
*
|
|
9025
|
+
* ## Why this needs the module's own `.val.ts`
|
|
9026
|
+
*
|
|
9027
|
+
* There is no derivable relationship between a record key and its entry file.
|
|
9028
|
+
* From the example app:
|
|
9029
|
+
*
|
|
9030
|
+
* "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
|
|
9031
|
+
* "/support/faq": c.json(() => import("./content/faq.val.json"))
|
|
9032
|
+
*
|
|
9033
|
+
* The key is a route and the path is whatever the author wrote. The only place
|
|
9034
|
+
* the two are related is the `import()` literal inside the `.val.ts`, which is
|
|
9035
|
+
* why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
|
|
9036
|
+
* commit archive stores the module's SOURCE (the markers) and its schema, not
|
|
9037
|
+
* its text - so the text is fetched from git at that commit and parsed here.
|
|
9038
|
+
*
|
|
9039
|
+
* Two round trips per entry, then: the module, and the entry. The module's
|
|
9040
|
+
* parse is worth caching per (commit, module) by the caller; nothing here does,
|
|
9041
|
+
* because a `ValOps` is not a cache and the endpoint above it is the layer that
|
|
9042
|
+
* knows how long a commit stays interesting.
|
|
9043
|
+
*/
|
|
9044
|
+
async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
|
|
9045
|
+
const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
|
|
9046
|
+
if (result.isErr(moduleGitPath)) {
|
|
9047
|
+
return moduleGitPath;
|
|
9048
|
+
}
|
|
9049
|
+
const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
|
|
9050
|
+
if (result.isErr(moduleRes)) {
|
|
9051
|
+
return moduleRes;
|
|
9052
|
+
}
|
|
9053
|
+
const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
|
|
9054
|
+
if (result.isErr(entryPath)) {
|
|
9055
|
+
return entryPath;
|
|
9056
|
+
}
|
|
9057
|
+
// The entry path resolved above is PROJECT-relative, like the module path it
|
|
9058
|
+
// came from; the git path needs the root in front of it, and the ops are what
|
|
9059
|
+
// know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
|
|
9060
|
+
// a `.val.json` is not a module, but "project-relative path -> repo path" is
|
|
9061
|
+
// the same question and one implementation of it is the point.
|
|
9062
|
+
const entryGitPath = ops.gitPathOfModule(entryPath.value);
|
|
9063
|
+
if (result.isErr(entryGitPath)) {
|
|
9064
|
+
return entryGitPath;
|
|
9065
|
+
}
|
|
9066
|
+
const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
|
|
9067
|
+
if (result.isErr(entryRes)) {
|
|
9068
|
+
return entryRes;
|
|
9069
|
+
}
|
|
9070
|
+
try {
|
|
9071
|
+
return result.ok(JSON.parse(entryRes.value.toString("utf-8")));
|
|
9072
|
+
} catch (err) {
|
|
9073
|
+
return result.err({
|
|
9074
|
+
kind: "file-unavailable",
|
|
9075
|
+
gitPath: entryGitPath.value,
|
|
9076
|
+
message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
|
|
9077
|
+
});
|
|
9078
|
+
}
|
|
9079
|
+
}
|
|
9080
|
+
|
|
9081
|
+
/**
|
|
9082
|
+
* The `import()` path a key's entry is behind, from the module's text.
|
|
9083
|
+
*
|
|
9084
|
+
* Exported for its own test: this is the part with a real chance of being
|
|
9085
|
+
* wrong, and it is pure.
|
|
9086
|
+
*/
|
|
9087
|
+
function findEntryImportPath(moduleFilePath,
|
|
9088
|
+
/**
|
|
9089
|
+
* The same module, repository-relative, for error reporting only.
|
|
9090
|
+
*
|
|
9091
|
+
* A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
|
|
9092
|
+
* other history helper reports, and what a reader can paste into a `git show`
|
|
9093
|
+
* - so reporting the project-relative ModuleFilePath here named a file that
|
|
9094
|
+
* does not exist at that path for any project not rooted at the repo root.
|
|
9095
|
+
*/
|
|
9096
|
+
moduleGitPath, valTsSource, key) {
|
|
9097
|
+
const sourceFile = ts.createSourceFile(moduleFilePath, valTsSource, ts.ScriptTarget.ES2015, true);
|
|
9098
|
+
let analysis;
|
|
9099
|
+
try {
|
|
9100
|
+
analysis = analyzeValModule(sourceFile);
|
|
9101
|
+
} catch (err) {
|
|
9102
|
+
// `file-unavailable` rather than a kind of its own: the module's TEXT is
|
|
9103
|
+
// the file we could not use, and adding a wire kind for "unparseable"
|
|
9104
|
+
// would be a new case every reader has to handle to say the same thing.
|
|
9105
|
+
return result.err({
|
|
9106
|
+
kind: "file-unavailable",
|
|
9107
|
+
gitPath: moduleGitPath,
|
|
9108
|
+
message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
|
|
9109
|
+
});
|
|
9110
|
+
}
|
|
9111
|
+
if (result.isErr(analysis)) {
|
|
9112
|
+
return result.err({
|
|
9113
|
+
kind: "file-unavailable",
|
|
9114
|
+
gitPath: moduleGitPath,
|
|
9115
|
+
message: "could not read the module at this commit"
|
|
9116
|
+
});
|
|
9117
|
+
}
|
|
9118
|
+
const entries = analyzeJsonValuesEntries(analysis.value.source);
|
|
9119
|
+
const entry = entries.get(key);
|
|
9120
|
+
if (entry === undefined) {
|
|
9121
|
+
/*
|
|
9122
|
+
* Reported, not treated as empty. A key with no entry at this commit means
|
|
9123
|
+
* the entry was added later - so "there is nothing to show for it here" is
|
|
9124
|
+
* the true answer, and rendering an empty field instead would say the
|
|
9125
|
+
* author had left it blank.
|
|
9126
|
+
*/
|
|
9127
|
+
return result.err({
|
|
9128
|
+
kind: "file-unavailable",
|
|
9129
|
+
gitPath: moduleGitPath,
|
|
9130
|
+
message: `'${key}' had no entry at this commit`
|
|
9131
|
+
});
|
|
9132
|
+
}
|
|
9133
|
+
const moduleDir = path__default.posix.dirname(moduleFilePath);
|
|
9134
|
+
return result.ok(path__default.posix.join(moduleDir, entry.importPath));
|
|
9135
|
+
}
|
|
9136
|
+
|
|
8982
9137
|
const host = process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST;
|
|
8983
9138
|
const SettingsSchema = z.object({
|
|
8984
9139
|
publicProjectId: z.string(),
|
|
@@ -12274,12 +12429,60 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12274
12429
|
};
|
|
12275
12430
|
}
|
|
12276
12431
|
},
|
|
12432
|
+
"/history/json": {
|
|
12433
|
+
GET: async req => {
|
|
12434
|
+
// Authenticated, for the reasons on /history/files below.
|
|
12435
|
+
const auth = getAuth(req.cookies);
|
|
12436
|
+
if (auth.error) {
|
|
12437
|
+
return {
|
|
12438
|
+
status: 401,
|
|
12439
|
+
json: {
|
|
12440
|
+
message: auth.error
|
|
12441
|
+
}
|
|
12442
|
+
};
|
|
12443
|
+
}
|
|
12444
|
+
const res = await getJsonEntryAtCommit(serverOps, req.query.commit_sha, req.query.path, req.query.key);
|
|
12445
|
+
if (result.isErr(res)) {
|
|
12446
|
+
return historyErrorResponse(res.error);
|
|
12447
|
+
}
|
|
12448
|
+
return {
|
|
12449
|
+
status: 200,
|
|
12450
|
+
json: {
|
|
12451
|
+
path: req.query.path,
|
|
12452
|
+
key: req.query.key,
|
|
12453
|
+
content: res.value
|
|
12454
|
+
}
|
|
12455
|
+
};
|
|
12456
|
+
}
|
|
12457
|
+
},
|
|
12277
12458
|
"/history/files": {
|
|
12278
12459
|
GET: async req => {
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12460
|
+
/*
|
|
12461
|
+
* Authenticated, unlike /files.
|
|
12462
|
+
*
|
|
12463
|
+
* This used to reason "same as /files" and that was wrong twice over.
|
|
12464
|
+
* /files is open because a `patch_id` is an unguessable UUID standing
|
|
12465
|
+
* in for a credential, and because it HAS to be: a draft image is
|
|
12466
|
+
* fetched by the app's own backend during Next image optimisation,
|
|
12467
|
+
* backend-to-backend, with no cookies to send.
|
|
12468
|
+
*
|
|
12469
|
+
* A commit sha is not a secret - it is in `git log`, in the GitHub UI,
|
|
12470
|
+
* on every PR - so the first argument does not transfer. And nothing
|
|
12471
|
+
* fetches this server-side; both callers are the Studio in a browser
|
|
12472
|
+
* that has the session cookie (the history pane's <img>, and
|
|
12473
|
+
* stageRestore's fetch, both same-origin so the cookie goes). So the
|
|
12474
|
+
* second does not either, and there is nothing to trade away by
|
|
12475
|
+
* asking.
|
|
12476
|
+
*/
|
|
12477
|
+
const auth = getAuth(req.cookies);
|
|
12478
|
+
if (auth.error) {
|
|
12479
|
+
return {
|
|
12480
|
+
status: 401,
|
|
12481
|
+
json: {
|
|
12482
|
+
message: auth.error
|
|
12483
|
+
}
|
|
12484
|
+
};
|
|
12485
|
+
}
|
|
12283
12486
|
const res = await serverOps.getFileAtCommit(req.query.commit_sha, req.query.path, req.query.remote === "true");
|
|
12284
12487
|
if (result.isErr(res)) {
|
|
12285
12488
|
const response = historyErrorResponse(res.error);
|
|
@@ -12318,6 +12521,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12318
12521
|
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
|
12319
12522
|
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
12320
12523
|
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
12524
|
+
// Note that /history/files, which looks like the same endpoint, IS
|
|
12525
|
+
// authenticated: its token is a commit sha, which is published, and
|
|
12526
|
+
// nothing fetches it backend-to-backend. Neither half of the argument
|
|
12527
|
+
// above transfers. See architecture/media.md, "Why /files has no auth,
|
|
12528
|
+
// and /history/files does".
|
|
12321
12529
|
let fileBuffer;
|
|
12322
12530
|
let mimeType;
|
|
12323
12531
|
const remote = query.remote === "true";
|
|
@@ -14682,7 +14890,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
14682
14890
|
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
14683
14891
|
}
|
|
14684
14892
|
|
|
14685
|
-
// Gallery (s.images({
|
|
14893
|
+
// Gallery (s.images({ ... }).remote() / s.files({ ... }).remote()) upload.
|
|
14686
14894
|
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
14687
14895
|
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
14688
14896
|
// ref from the key and synthesize the image/file schema from the record's
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"types": "dist/valbuild-server.cjs.d.ts",
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.125.0",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0"
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
31
|
"zod-validation-error": "^5.0.0",
|
|
32
|
-
"@valbuild/core": "0.
|
|
33
|
-
"@valbuild/
|
|
34
|
-
"@valbuild/
|
|
32
|
+
"@valbuild/core": "0.125.0",
|
|
33
|
+
"@valbuild/shared": "0.125.0",
|
|
34
|
+
"@valbuild/ui": "0.125.0"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": "^20.19.0 || >=22"
|