@valbuild/server 0.129.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 +96 -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
|
@@ -2529,6 +2529,17 @@ class ValOps {
|
|
|
2529
2529
|
adopt[moduleFilePath] = source;
|
|
2530
2530
|
}
|
|
2531
2531
|
this.promoteCommittedSources(adopt);
|
|
2532
|
+
/**
|
|
2533
|
+
* And the `.val.ts` TEXT, for a store that has nowhere else to keep it.
|
|
2534
|
+
*
|
|
2535
|
+
* Everything above adopts the SOURCE — the data a module evaluates to. The
|
|
2536
|
+
* file the patch was applied TO is a separate thing, and `getSourceFile` is
|
|
2537
|
+
* what the next `prepare()` reads. In `fs` mode that is the disk, which
|
|
2538
|
+
* `saveOrUploadFiles` has just rewritten, so this is a no-op. A store with
|
|
2539
|
+
* no disk has to be told, or every later save re-reads the source as it was
|
|
2540
|
+
* when the process started and silently reverts this one.
|
|
2541
|
+
*/
|
|
2542
|
+
this.adoptPatchedSourceFiles(preparedCommit.patchedSourceFiles);
|
|
2532
2543
|
/**
|
|
2533
2544
|
* And the `.jsonValues()` entry content, which the sources above do not
|
|
2534
2545
|
* carry — they hold markers. See {@link adoptedJsonEntries}.
|
|
@@ -4087,7 +4098,56 @@ class ValOps {
|
|
|
4087
4098
|
});
|
|
4088
4099
|
}
|
|
4089
4100
|
|
|
4101
|
+
/**
|
|
4102
|
+
* Take the `.val.ts` text a commit produced as the new committed source.
|
|
4103
|
+
*
|
|
4104
|
+
* A no-op where {@link getSourceFile} reads something the commit already
|
|
4105
|
+
* wrote — the disk in `fs` mode, the content service in `http` mode. Override
|
|
4106
|
+
* it in a store that holds the source itself. `null` means the commit deleted
|
|
4107
|
+
* the file.
|
|
4108
|
+
*/
|
|
4109
|
+
adoptPatchedSourceFiles(_files) {}
|
|
4110
|
+
|
|
4090
4111
|
// #region abstract ops
|
|
4112
|
+
/**
|
|
4113
|
+
* Whether the patches live HERE, in this server, or in Val's content service.
|
|
4114
|
+
*
|
|
4115
|
+
* Almost everything the routes branch on comes from this one fact, which is
|
|
4116
|
+
* why it is a named property rather than an `instanceof`. If this server owns
|
|
4117
|
+
* the store then there is no content service to authenticate to (so an absent
|
|
4118
|
+
* or unverifiable session is anonymous rather than a 401), no shared store for
|
|
4119
|
+
* a patch group to separate authors in, no deployments to report, and a
|
|
4120
|
+
* "publish" writes what the host does with it rather than pushing a commit. If
|
|
4121
|
+
* it does not, every one of those is the content service's and this server is
|
|
4122
|
+
* relaying.
|
|
4123
|
+
*
|
|
4124
|
+
* There were two implementations when the routes were written and `instanceof
|
|
4125
|
+
* ValOpsFS` meant this; a third made that reading wrong in a way that compiles
|
|
4126
|
+
* silently -- a store that is local, answers none of the checks, and gets the
|
|
4127
|
+
* http path with no content service behind it.
|
|
4128
|
+
*
|
|
4129
|
+
* It is also what `/stat` reports as `mode`. The wire name predates the third
|
|
4130
|
+
* implementation and names a class, but the question the client is asking is
|
|
4131
|
+
* this one: does it auto-save and hide the account panel, or does it publish.
|
|
4132
|
+
*/
|
|
4133
|
+
|
|
4134
|
+
/**
|
|
4135
|
+
* Whether a request must carry a session this server verified.
|
|
4136
|
+
*
|
|
4137
|
+
* Split out of {@link patchesAreLocal}, which was answering two questions at
|
|
4138
|
+
* once. "Does this store auto-save or publish" is a BEHAVIOUR question, and
|
|
4139
|
+
* it is what `/stat` reports and the UI keys off. "May an unauthenticated
|
|
4140
|
+
* request write here" is a SECURITY question. With two implementations the
|
|
4141
|
+
* answers coincided -- fs is local dev where no credential exists, http is
|
|
4142
|
+
* remote -- so one flag served both and nothing noticed.
|
|
4143
|
+
*
|
|
4144
|
+
* A third implementation splits them. `ValOpsMemory`'s store is local, which
|
|
4145
|
+
* makes the first answer yes, and it is designed to run DEPLOYED, which makes
|
|
4146
|
+
* the second answer no. Reusing one flag gave a deployed host `getAuth`
|
|
4147
|
+
* returning anonymous success for a missing cookie, an invalid JWT, an
|
|
4148
|
+
* unparseable payload, or no configured secret -- on all 29 routes, including
|
|
4149
|
+
* the ones that create patches and publish.
|
|
4150
|
+
*/
|
|
4091
4151
|
|
|
4092
4152
|
/**
|
|
4093
4153
|
* Save a patch's binary file from a `data:...;base64,...` URL.
|
|
@@ -5684,6 +5744,13 @@ function serializeSchemaSafely(schema) {
|
|
|
5684
5744
|
class ValOpsFS extends ValOps {
|
|
5685
5745
|
static VAL_DIR = ".val";
|
|
5686
5746
|
host;
|
|
5747
|
+
/** The developer's own working tree. See {@link ValOps.patchesAreLocal}. */
|
|
5748
|
+
patchesAreLocal = true;
|
|
5749
|
+
/**
|
|
5750
|
+
* The developer's own machine, where there is no credential to require and
|
|
5751
|
+
* nothing to protect it from. See {@link ValOps.requiresAuth}.
|
|
5752
|
+
*/
|
|
5753
|
+
requiresAuth = false;
|
|
5687
5754
|
constructor(contentUrl, rootDir, valModules, options) {
|
|
5688
5755
|
super(valModules, options);
|
|
5689
5756
|
this.contentUrl = contentUrl;
|
|
@@ -7228,6 +7295,10 @@ const PATCH_GROUPS_CACHE_MS = 1000;
|
|
|
7228
7295
|
class ValOpsHttp extends ValOps {
|
|
7229
7296
|
authHeaders;
|
|
7230
7297
|
root;
|
|
7298
|
+
/** Val's content service owns the store. See {@link ValOps.patchesAreLocal}. */
|
|
7299
|
+
patchesAreLocal = false;
|
|
7300
|
+
/** See {@link ValOps.requiresAuth}. */
|
|
7301
|
+
requiresAuth = true;
|
|
7231
7302
|
constructor(contentUrl, project, commitSha,
|
|
7232
7303
|
// TODO: CommitSha
|
|
7233
7304
|
branch,
|
|
@@ -8659,6 +8730,643 @@ class ValOpsHttp extends ValOps {
|
|
|
8659
8730
|
// #endregion history
|
|
8660
8731
|
}
|
|
8661
8732
|
|
|
8733
|
+
/**
|
|
8734
|
+
* One stored patch. Everything the ordered chain needs and nothing else.
|
|
8735
|
+
*/
|
|
8736
|
+
|
|
8737
|
+
/**
|
|
8738
|
+
* One pending binary file: an upload that has not been published yet.
|
|
8739
|
+
*
|
|
8740
|
+
* Keyed by the patch that carries it, exactly as `fs` mode keys the directory
|
|
8741
|
+
* it writes to. The patch's own `file` op holds a hash, not the bytes, so these
|
|
8742
|
+
* arrive on their own request and are joined up by `(patchId, filePath)`.
|
|
8743
|
+
*/
|
|
8744
|
+
|
|
8745
|
+
/**
|
|
8746
|
+
* Where pending patches live.
|
|
8747
|
+
*
|
|
8748
|
+
* The point of the interface: `ValOpsMemory` is not tied to memory. The default
|
|
8749
|
+
* implementation below is explicitly NOT durable -- it dies with the process,
|
|
8750
|
+
* or in a Worker with the isolate -- and a durable one (a Durable Object, whose
|
|
8751
|
+
* single-threaded execution is the lock Val's fs store builds out of a file)
|
|
8752
|
+
* is a swap rather than a rewrite.
|
|
8753
|
+
*
|
|
8754
|
+
* ORDER IS THE CONTRACT. `list()` returns patch ids in the order they were
|
|
8755
|
+
* written, and that order is the chain: entry i's parent is entry i-1. This is
|
|
8756
|
+
* the same decision `ValOpsFS` makes with `patches.log` -- the server decides
|
|
8757
|
+
* where a patch goes, and it goes last -- and it exists for the same reason: an
|
|
8758
|
+
* order held in the patches themselves lets a client working from a stale view
|
|
8759
|
+
* strand every patch behind a parent that never landed.
|
|
8760
|
+
*/
|
|
8761
|
+
|
|
8762
|
+
/** The default store. Not durable, deliberately and visibly. */
|
|
8763
|
+
class InMemoryPatchStore {
|
|
8764
|
+
order = [];
|
|
8765
|
+
byId = new Map();
|
|
8766
|
+
async list() {
|
|
8767
|
+
return [...this.order];
|
|
8768
|
+
}
|
|
8769
|
+
async get(patchId) {
|
|
8770
|
+
return this.byId.get(patchId) ?? null;
|
|
8771
|
+
}
|
|
8772
|
+
async append(patch) {
|
|
8773
|
+
if (!this.byId.has(patch.patchId)) {
|
|
8774
|
+
this.order.push(patch.patchId);
|
|
8775
|
+
}
|
|
8776
|
+
this.byId.set(patch.patchId, patch);
|
|
8777
|
+
}
|
|
8778
|
+
async delete(patchIds) {
|
|
8779
|
+
for (const patchId of patchIds) {
|
|
8780
|
+
const at = this.order.indexOf(patchId);
|
|
8781
|
+
if (at !== -1) {
|
|
8782
|
+
this.order.splice(at, 1);
|
|
8783
|
+
}
|
|
8784
|
+
this.byId.delete(patchId);
|
|
8785
|
+
// The bytes go with the patch. They are only reachable through it, so
|
|
8786
|
+
// keeping them would be a leak with no reader -- and these are images.
|
|
8787
|
+
for (const key of this.files.keys()) {
|
|
8788
|
+
if (key.startsWith(`${patchId}\u0000`)) {
|
|
8789
|
+
this.files.delete(key);
|
|
8790
|
+
}
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
|
|
8795
|
+
/*
|
|
8796
|
+
* `\u0000` cannot occur in a patch id or a path, so the two halves of the key
|
|
8797
|
+
* can never run together into a different pair.
|
|
8798
|
+
*
|
|
8799
|
+
* The PATH is normalised for the same reason `sourceFiles` is, and it is not
|
|
8800
|
+
* cosmetic: an upload arrives as `/public/val/x.png`, and the publish step
|
|
8801
|
+
* looks the same file up as `public/val/x.png` because that is what
|
|
8802
|
+
* `splitRemoteRef` yields from a remote ref. `ValOpsFS` never noticed --
|
|
8803
|
+
* `path.join` collapses the difference -- so this is another thing a
|
|
8804
|
+
* filesystem was doing for free. Without it a remote image uploads fine,
|
|
8805
|
+
* patches fine, and fails at publish with "No bytes held", naming a ref whose
|
|
8806
|
+
* bytes are sitting right there under the other spelling.
|
|
8807
|
+
*/
|
|
8808
|
+
static fileKey(patchId, filePath) {
|
|
8809
|
+
return `${patchId}\u0000${filePath.replace(/^\//, "")}`;
|
|
8810
|
+
}
|
|
8811
|
+
files = new Map();
|
|
8812
|
+
async putFile(file) {
|
|
8813
|
+
this.files.set(InMemoryPatchStore.fileKey(file.patchId, file.filePath), file);
|
|
8814
|
+
}
|
|
8815
|
+
async getFile(patchId, filePath) {
|
|
8816
|
+
return this.files.get(InMemoryPatchStore.fileKey(patchId, filePath)) ?? null;
|
|
8817
|
+
}
|
|
8818
|
+
async deleteFile(patchId, filePath) {
|
|
8819
|
+
this.files.delete(InMemoryPatchStore.fileKey(patchId, filePath));
|
|
8820
|
+
}
|
|
8821
|
+
async filesOf(patchId) {
|
|
8822
|
+
return [...this.files.values()].filter(file => file.patchId === patchId);
|
|
8823
|
+
}
|
|
8824
|
+
}
|
|
8825
|
+
/**
|
|
8826
|
+
* A `ValOps` for a host that is neither a developer's machine nor
|
|
8827
|
+
* content.val.build.
|
|
8828
|
+
*
|
|
8829
|
+
* EXPERIMENTAL -- see VAL_PROMPT.md.
|
|
8830
|
+
*
|
|
8831
|
+
* `fs` mode assumes a working tree it can watch and write; `http` mode assumes
|
|
8832
|
+
* the content API owns the patch chain and a commit means a git commit. A host
|
|
8833
|
+
* that builds and publishes its own output is neither: it holds the source
|
|
8834
|
+
* already, it has nowhere to watch, and its "commit" is a new build.
|
|
8835
|
+
*
|
|
8836
|
+
* What this deliberately does NOT do:
|
|
8837
|
+
*
|
|
8838
|
+
* - **No filesystem.** Source comes from `sourceFiles`, patches from a store.
|
|
8839
|
+
* - **No watching.** `getStat` still long-polls -- the hold is what paces the
|
|
8840
|
+
* client -- but it parks on a signal rather than racing a timer against an
|
|
8841
|
+
* mtime poll that can never observe anything here. Nothing can edit files
|
|
8842
|
+
* behind Val's back: source changes only when the host publishes, and that
|
|
8843
|
+
* replaces the process.
|
|
8844
|
+
* - **Pending binary files, but no PUBLISHED local ones.** An upload is held in
|
|
8845
|
+
* the patch store like any other pending change, so the Studio can preview it
|
|
8846
|
+
* before it is published. What this has no answer for is a file that is
|
|
8847
|
+
* already published and served from a `/public` directory: this configuration
|
|
8848
|
+
* uses Val's REMOTE files, where a published image lives on the content host
|
|
8849
|
+
* and the source carries a URL. `getBinaryFile` answers `null` for those --
|
|
8850
|
+
* a miss, not a fault -- and `getBinaryFileMetadata` refuses by name.
|
|
8851
|
+
* - **No git history.** There is no repository here, so the history methods
|
|
8852
|
+
* answer `not-supported-in-fs-mode` -- the same closed error `ValOpsFS`
|
|
8853
|
+
* uses, so the History UI degrades the way it already knows how rather than
|
|
8854
|
+
* inventing a commit list. See the note above `listCommits`.
|
|
8855
|
+
*/
|
|
8856
|
+
class ValOpsMemory extends ValOps {
|
|
8857
|
+
/**
|
|
8858
|
+
* The host's own store -- see {@link ValOps.patchesAreLocal}. `true` for the
|
|
8859
|
+
* same reason `fs` mode is: nothing is relayed to a content service, so
|
|
8860
|
+
* there is no session to verify against one and no group to separate authors
|
|
8861
|
+
* in. Where the two differ is not something a route asks about.
|
|
8862
|
+
*/
|
|
8863
|
+
patchesAreLocal = true;
|
|
8864
|
+
/**
|
|
8865
|
+
* Required, unless the host explicitly takes the boundary itself.
|
|
8866
|
+
*
|
|
8867
|
+
* `patchesAreLocal` is true here and that is about publishing, not about who
|
|
8868
|
+
* may write -- see {@link ValOps.requiresAuth}.
|
|
8869
|
+
*/
|
|
8870
|
+
requiresAuth;
|
|
8871
|
+
store;
|
|
8872
|
+
/**
|
|
8873
|
+
* The project's source, keyed WITHOUT a leading slash.
|
|
8874
|
+
*
|
|
8875
|
+
* Two spellings reach this. A host keys by project-relative path
|
|
8876
|
+
* (`src/routes/page.val.ts`) because that is what it built from; Val asks and
|
|
8877
|
+
* commits with a leading slash (`/src/routes/page.val.ts`). Normalised on the
|
|
8878
|
+
* way in so there is one entry per file — holding both spellings would let a
|
|
8879
|
+
* commit update one and leave the other as the stale answer.
|
|
8880
|
+
*
|
|
8881
|
+
* Not `readonly`: a save replaces the files it rewrote. See
|
|
8882
|
+
* {@link adoptPatchedSourceFiles}.
|
|
8883
|
+
*/
|
|
8884
|
+
sourceFiles;
|
|
8885
|
+
contentUrl;
|
|
8886
|
+
constructor(valModules, options) {
|
|
8887
|
+
super(valModules, options);
|
|
8888
|
+
this.store = options.patchStore ?? new InMemoryPatchStore();
|
|
8889
|
+
this.contentUrl = options.contentUrl;
|
|
8890
|
+
this.requiresAuth = !options.unsafelyAllowUnauthenticated;
|
|
8891
|
+
if (options.unsafelyAllowUnauthenticated) {
|
|
8892
|
+
console.warn("Val: serving memory mode WITHOUT authentication. Every request that " + "reaches this server may read content, create patches and trigger a " + "publish. Only correct if the host authorises requests before they " + "get here.");
|
|
8893
|
+
}
|
|
8894
|
+
this.sourceFiles = Object.fromEntries(Object.entries(options.sourceFiles).map(([path, text]) => [ValOpsMemory.key(path), text]));
|
|
8895
|
+
}
|
|
8896
|
+
|
|
8897
|
+
// #region the six that carry the mode
|
|
8898
|
+
|
|
8899
|
+
async onInit() {
|
|
8900
|
+
// Nothing to prepare: there is no directory to create and no store to open.
|
|
8901
|
+
}
|
|
8902
|
+
|
|
8903
|
+
/**
|
|
8904
|
+
* Requests parked in {@link getStat}, waiting for something to happen.
|
|
8905
|
+
*
|
|
8906
|
+
* See there for why this exists. Resolved and emptied by
|
|
8907
|
+
* {@link announceChange}; never rejected, because a waiter that gives up does
|
|
8908
|
+
* so on its own timeout.
|
|
8909
|
+
*/
|
|
8910
|
+
statWaiters = [];
|
|
8911
|
+
|
|
8912
|
+
/**
|
|
8913
|
+
* Writes so far. Sampled before reading, compared after registering.
|
|
8914
|
+
*
|
|
8915
|
+
* The registration is not atomic with the read above it: a patch written
|
|
8916
|
+
* between `currentStat()` and `statWaiters.push` was announced to a list this
|
|
8917
|
+
* waiter was not yet on, so the request slept the full interval with a change
|
|
8918
|
+
* already sitting there. Comparing the count closes that window without a
|
|
8919
|
+
* lock.
|
|
8920
|
+
*/
|
|
8921
|
+
changeCount = 0;
|
|
8922
|
+
|
|
8923
|
+
/** Wake every parked `getStat`. Called by this instance's own writes. */
|
|
8924
|
+
announceChange() {
|
|
8925
|
+
this.changeCount += 1;
|
|
8926
|
+
const waiting = this.statWaiters;
|
|
8927
|
+
this.statWaiters = [];
|
|
8928
|
+
for (const wake of waiting) {
|
|
8929
|
+
wake();
|
|
8930
|
+
}
|
|
8931
|
+
}
|
|
8932
|
+
async currentStat() {
|
|
8933
|
+
return {
|
|
8934
|
+
baseSha: await this.getBaseSha(),
|
|
8935
|
+
schemaSha: await this.getSchemaSha(),
|
|
8936
|
+
sourcesSha: await this.getSourcesSha(),
|
|
8937
|
+
patches: await this.store.list()
|
|
8938
|
+
};
|
|
8939
|
+
}
|
|
8940
|
+
async getStat(params) {
|
|
8941
|
+
/*
|
|
8942
|
+
* A long poll, and it has to be one -- but parked on a SIGNAL rather than a
|
|
8943
|
+
* timer.
|
|
8944
|
+
*
|
|
8945
|
+
* The hold is what paces the client. `useStatus.ts` sets `wait: 0` between
|
|
8946
|
+
* stats unless it has a WebSocket, with the comment "we are long polling so
|
|
8947
|
+
* no point in waiting": the server holding the request open IS the rate
|
|
8948
|
+
* limit. An earlier version of this method answered immediately, on the
|
|
8949
|
+
* reasoning that nothing can edit files behind Val's back in an isolate --
|
|
8950
|
+
* true, and beside the point. It turned a 20s poll into a request every
|
|
8951
|
+
* 6ms, which is worse than what it replaced.
|
|
8952
|
+
*
|
|
8953
|
+
* What was actually wrong with `fs` mode here is not the hold, it is the
|
|
8954
|
+
* WATCHING: it races a 250ms mtime poll and an `fs.watch`, neither of which
|
|
8955
|
+
* can observe anything in an isolate (the shim's `watch` never fires and
|
|
8956
|
+
* mtime is always 0), so it burns CPU for 20s to learn nothing. This owns
|
|
8957
|
+
* its store, so it is TOLD instead -- zero timers while parked, and a patch
|
|
8958
|
+
* written by another tab is seen at once rather than up to 250ms later.
|
|
8959
|
+
*
|
|
8960
|
+
* The timeout is the backstop, and it is not only for an idle branch: a
|
|
8961
|
+
* store shared between isolates (a Durable Object -- see ValPatchStore) can
|
|
8962
|
+
* change without this instance writing anything, and nothing would announce
|
|
8963
|
+
* that. So it re-reads on the way out rather than assuming `no-change`.
|
|
8964
|
+
*/
|
|
8965
|
+
// Before the read, not after: anything that happens from here on must be
|
|
8966
|
+
// observed, either by `differs` below or by the recheck in parkUntilChange.
|
|
8967
|
+
const seenAt = this.changeCount;
|
|
8968
|
+
const before = await this.currentStat();
|
|
8969
|
+
const differs = now => params === null || params.baseSha !== now.baseSha || params.schemaSha !== now.schemaSha || (params.patches ?? []).join(",") !== now.patches.join(",");
|
|
8970
|
+
|
|
8971
|
+
// Already behind: answer now. This is the case that matters for latency --
|
|
8972
|
+
// the client has just written a patch and is asking what happened.
|
|
8973
|
+
if (differs(before)) {
|
|
8974
|
+
return {
|
|
8975
|
+
type: "did-change",
|
|
8976
|
+
...before
|
|
8977
|
+
};
|
|
8978
|
+
}
|
|
8979
|
+
await this.parkUntilChange(seenAt);
|
|
8980
|
+
const after = await this.currentStat();
|
|
8981
|
+
return {
|
|
8982
|
+
type: differs(after) ? "did-change" : "no-change",
|
|
8983
|
+
...after
|
|
8984
|
+
};
|
|
8985
|
+
}
|
|
8986
|
+
|
|
8987
|
+
/** Resolves on the next write here, or when the poll interval runs out. */
|
|
8988
|
+
parkUntilChange(seenAt) {
|
|
8989
|
+
var _this$options;
|
|
8990
|
+
const timeoutMs = ((_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.statPollingInterval) ?? 20_000;
|
|
8991
|
+
return new Promise(resolve => {
|
|
8992
|
+
let settled = false;
|
|
8993
|
+
const done = () => {
|
|
8994
|
+
if (settled) return;
|
|
8995
|
+
settled = true;
|
|
8996
|
+
// Cleared on BOTH paths: a change that wins the race leaves a 20s timer
|
|
8997
|
+
// behind otherwise, and in a Worker a pending timer is a reason to keep
|
|
8998
|
+
// the isolate alive.
|
|
8999
|
+
clearTimeout(timer);
|
|
9000
|
+
// Removed on BOTH paths too, for the same kind of reason. Only
|
|
9001
|
+
// `announceChange` emptied this list, so a timed-out waiter stayed on
|
|
9002
|
+
// it forever: one dead closure per idle poll, on a server that polls
|
|
9003
|
+
// every 20 seconds indefinitely, and a later write walked all of them.
|
|
9004
|
+
const at = this.statWaiters.indexOf(done);
|
|
9005
|
+
if (at !== -1) {
|
|
9006
|
+
this.statWaiters.splice(at, 1);
|
|
9007
|
+
}
|
|
9008
|
+
resolve();
|
|
9009
|
+
};
|
|
9010
|
+
const timer = setTimeout(done, timeoutMs);
|
|
9011
|
+
this.statWaiters.push(done);
|
|
9012
|
+
// Registered; now look again. A write in the gap announced to a list this
|
|
9013
|
+
// waiter was not on yet, and waiting 20s for news that already arrived is
|
|
9014
|
+
// the bug this closes.
|
|
9015
|
+
if (this.changeCount !== seenAt) {
|
|
9016
|
+
done();
|
|
9017
|
+
}
|
|
9018
|
+
});
|
|
9019
|
+
}
|
|
9020
|
+
async fetchPatches(filters) {
|
|
9021
|
+
// An empty `patchIds` means "no filter", not "none": both shipped
|
|
9022
|
+
// implementations read it that way and callers rely on it. See
|
|
9023
|
+
// `scopedModulePatches` in ValOps.ts, which says why at length.
|
|
9024
|
+
const requested = filters.patchIds && filters.patchIds.length > 0 ? new Set(filters.patchIds) : null;
|
|
9025
|
+
const order = await this.store.list();
|
|
9026
|
+
const patches = [];
|
|
9027
|
+
for (const patchId of order) {
|
|
9028
|
+
if (requested !== null && !requested.has(patchId)) {
|
|
9029
|
+
continue;
|
|
9030
|
+
}
|
|
9031
|
+
const stored = await this.store.get(patchId);
|
|
9032
|
+
if (!stored) {
|
|
9033
|
+
continue;
|
|
9034
|
+
}
|
|
9035
|
+
patches.push({
|
|
9036
|
+
patchId: stored.patchId,
|
|
9037
|
+
path: stored.path,
|
|
9038
|
+
patch: stored.patch,
|
|
9039
|
+
createdAt: stored.createdAt,
|
|
9040
|
+
authorId: stored.authorId,
|
|
9041
|
+
baseSha: stored.baseSha,
|
|
9042
|
+
// Nothing here is ever applied-at-a-commit: a publish in this mode
|
|
9043
|
+
// rebuilds the site and starts a new process, so a patch that has been
|
|
9044
|
+
// applied is a patch this store no longer holds.
|
|
9045
|
+
appliedAt: null
|
|
9046
|
+
});
|
|
9047
|
+
}
|
|
9048
|
+
// The cast is unavoidable: the return type is conditional on a generic
|
|
9049
|
+
// TypeScript cannot narrow from a value. `ValOpsFS` does the same.
|
|
9050
|
+
return {
|
|
9051
|
+
patches: filters.excludePatchOps ? patches.map(({
|
|
9052
|
+
patch: _patch,
|
|
9053
|
+
...rest
|
|
9054
|
+
}) => ({
|
|
9055
|
+
...rest,
|
|
9056
|
+
patch: undefined
|
|
9057
|
+
})) : patches
|
|
9058
|
+
};
|
|
9059
|
+
}
|
|
9060
|
+
async saveSourceFilePatch(path, patch, patchId,
|
|
9061
|
+
/*
|
|
9062
|
+
* Ignored, exactly as in `fs` mode, and named so that is visible.
|
|
9063
|
+
*
|
|
9064
|
+
* The store's order IS the chain, so the server decides where a patch goes
|
|
9065
|
+
* and it goes last. There is no parent to name, and so nothing that can
|
|
9066
|
+
* point at nothing. A patch computed against a different state is caught
|
|
9067
|
+
* where it shows -- applying it -- rather than by refusing the write.
|
|
9068
|
+
*/
|
|
9069
|
+
_parentRef, authorId, _sessionId,
|
|
9070
|
+
/* No shared store and no second author yet, so nothing for a group to
|
|
9071
|
+
* separate. Named rather than omitted: TypeScript allows an implementation
|
|
9072
|
+
* to take fewer parameters, so dropping it would look identical to
|
|
9073
|
+
* handling it. */
|
|
9074
|
+
_patchGroup) {
|
|
9075
|
+
await this.store.append({
|
|
9076
|
+
patchId,
|
|
9077
|
+
path,
|
|
9078
|
+
patch,
|
|
9079
|
+
authorId,
|
|
9080
|
+
createdAt: new Date().toISOString(),
|
|
9081
|
+
baseSha: await this.getBaseSha()
|
|
9082
|
+
});
|
|
9083
|
+
// Any `getStat` parked on this instance answers now instead of waiting out
|
|
9084
|
+
// its timeout. This is the whole reason the hold can be free: the store is
|
|
9085
|
+
// ours, so there is nothing to poll for.
|
|
9086
|
+
this.announceChange();
|
|
9087
|
+
return fp.result.ok({
|
|
9088
|
+
patchId
|
|
9089
|
+
});
|
|
9090
|
+
}
|
|
9091
|
+
|
|
9092
|
+
/** One spelling for a path, whichever the caller used. See sourceFiles. */
|
|
9093
|
+
static key(path) {
|
|
9094
|
+
return path.replace(/^\//, "");
|
|
9095
|
+
}
|
|
9096
|
+
|
|
9097
|
+
/**
|
|
9098
|
+
* A save has rewritten these files; they are the committed source now.
|
|
9099
|
+
*
|
|
9100
|
+
* Without this every save after the first re-reads the source as it was when
|
|
9101
|
+
* this object was built, applies only its own patches to that, and parks a
|
|
9102
|
+
* file that reverts everything saved before it -- with no error, because
|
|
9103
|
+
* applying the patch to the ORIGINAL text succeeds. The Studio auto-saves, so
|
|
9104
|
+
* that is not an edge case: it is most of a session's work.
|
|
9105
|
+
*
|
|
9106
|
+
* `fs` mode gets this for free -- `saveOrUploadFiles` writes the disk that
|
|
9107
|
+
* `getSourceFile` reads. There is no disk here, so it is written down.
|
|
9108
|
+
*/
|
|
9109
|
+
adoptPatchedSourceFiles(files) {
|
|
9110
|
+
for (const [path, text] of Object.entries(files)) {
|
|
9111
|
+
const key = ValOpsMemory.key(path);
|
|
9112
|
+
if (text === null) {
|
|
9113
|
+
delete this.sourceFiles[key];
|
|
9114
|
+
} else {
|
|
9115
|
+
this.sourceFiles[key] = text;
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
async getSourceFile(path) {
|
|
9120
|
+
const data = this.sourceFiles[ValOpsMemory.key(path)];
|
|
9121
|
+
if (data === undefined) {
|
|
9122
|
+
return {
|
|
9123
|
+
error: {
|
|
9124
|
+
message: `File not found: ${path}. In this mode the host hands Val its ` + `source, so a missing file means the host did not ship it -- not ` + `that it is absent from a disk.`
|
|
9125
|
+
}
|
|
9126
|
+
};
|
|
9127
|
+
}
|
|
9128
|
+
return {
|
|
9129
|
+
data
|
|
9130
|
+
};
|
|
9131
|
+
}
|
|
9132
|
+
async deletePatches(patchIds) {
|
|
9133
|
+
await this.store.delete(patchIds);
|
|
9134
|
+
this.announceChange();
|
|
9135
|
+
return {
|
|
9136
|
+
deleted: patchIds
|
|
9137
|
+
};
|
|
9138
|
+
}
|
|
9139
|
+
|
|
9140
|
+
/**
|
|
9141
|
+
* Push this commit's pending binary files to Val's content host.
|
|
9142
|
+
*
|
|
9143
|
+
* The half of publishing that `commitPrepared` cannot do. Val's remote files
|
|
9144
|
+
* upload at PUBLISH, not when the image is added: until then the bytes are a
|
|
9145
|
+
* pending change like any other, held by {@link ValPatchStore}. So a publish
|
|
9146
|
+
* has to walk the descriptors and push each one before the source that
|
|
9147
|
+
* references it goes live -- otherwise the new build ships a URL that 404s.
|
|
9148
|
+
*
|
|
9149
|
+
* `ValOpsFS.saveOrUploadFiles` does the same loop, alongside two things this
|
|
9150
|
+
* has no use for: copying LOCAL binaries into a working tree, and writing the
|
|
9151
|
+
* source files (which is `commitPrepared` here). Kept separate rather than
|
|
9152
|
+
* shared, because the shapes only look alike.
|
|
9153
|
+
*
|
|
9154
|
+
* Errors are collected rather than thrown. One image that will not upload
|
|
9155
|
+
* should name itself and leave the rest of the publish decidable, rather than
|
|
9156
|
+
* failing a save that has already applied its patches.
|
|
9157
|
+
*/
|
|
9158
|
+
async uploadRemoteFiles(preparedCommit, auth) {
|
|
9159
|
+
var _this$options2;
|
|
9160
|
+
const uploaded = [];
|
|
9161
|
+
const errors = {};
|
|
9162
|
+
const project = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.config.project;
|
|
9163
|
+
const descriptors = Object.entries(preparedCommit.patchedBinaryFilesDescriptors);
|
|
9164
|
+
const remote = descriptors.filter(([, descriptor]) => descriptor.remote);
|
|
9165
|
+
|
|
9166
|
+
/*
|
|
9167
|
+
* A LOCAL descriptor is refused, not ignored.
|
|
9168
|
+
*
|
|
9169
|
+
* This mode has no published local-file path -- there is no `/public` to
|
|
9170
|
+
* write into -- and the class docstring says so. But filtering to `remote`
|
|
9171
|
+
* meant a local one fell through silently, and the caller carries on:
|
|
9172
|
+
* source is adopted and the patch deleted, so `/save` answers 200 and the
|
|
9173
|
+
* uploaded bytes are gone with nothing anywhere saying why.
|
|
9174
|
+
*
|
|
9175
|
+
* Erroring here keeps the patch, because a save that cannot store what it
|
|
9176
|
+
* was given has not succeeded.
|
|
9177
|
+
*/
|
|
9178
|
+
for (const [ref, descriptor] of descriptors) {
|
|
9179
|
+
if (descriptor.remote) continue;
|
|
9180
|
+
errors[ref] = {
|
|
9181
|
+
message: "Cannot publish a local file in this mode: there is no directory to " + "publish it to. Configure the project for Val's remote files, which " + "is what this configuration uploads."
|
|
9182
|
+
};
|
|
9183
|
+
}
|
|
9184
|
+
if (remote.length === 0) {
|
|
9185
|
+
return {
|
|
9186
|
+
uploaded,
|
|
9187
|
+
errors
|
|
9188
|
+
};
|
|
9189
|
+
}
|
|
9190
|
+
if (!this.contentUrl || !project) {
|
|
9191
|
+
// Named separately from a failed upload: nothing was attempted, and the
|
|
9192
|
+
// fix is configuration rather than a retry.
|
|
9193
|
+
for (const [ref] of remote) {
|
|
9194
|
+
errors[ref] = {
|
|
9195
|
+
message: "Cannot publish a remote file: this server has no " + (!project ? "`project` in val.config" : "content host configured") + ". Remote files need both, plus an api key."
|
|
9196
|
+
};
|
|
9197
|
+
}
|
|
9198
|
+
return {
|
|
9199
|
+
uploaded,
|
|
9200
|
+
errors
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
9203
|
+
for (const [ref, {
|
|
9204
|
+
patchId
|
|
9205
|
+
}] of remote) {
|
|
9206
|
+
const split = core.Internal.remote.splitRemoteRef(ref);
|
|
9207
|
+
if (split.status === "error") {
|
|
9208
|
+
errors[ref] = {
|
|
9209
|
+
message: "Failed to split remote ref: " + ref
|
|
9210
|
+
};
|
|
9211
|
+
continue;
|
|
9212
|
+
}
|
|
9213
|
+
const bytes = await this.getBase64EncodedBinaryFileFromPatch(split.filePath, patchId);
|
|
9214
|
+
if (!bytes) {
|
|
9215
|
+
errors[ref] = {
|
|
9216
|
+
message: `No bytes held for ${ref} (patch ${patchId}). The upload either never arrived or was dropped with its patch.`
|
|
9217
|
+
};
|
|
9218
|
+
continue;
|
|
9219
|
+
}
|
|
9220
|
+
const res = await uploadRemoteFile(this.contentUrl, project, split.bucket, split.fileHash, getFileExt(split.filePath), bytes, auth);
|
|
9221
|
+
if (!res.success) {
|
|
9222
|
+
errors[ref] = {
|
|
9223
|
+
message: res.error
|
|
9224
|
+
};
|
|
9225
|
+
continue;
|
|
9226
|
+
}
|
|
9227
|
+
uploaded.push(ref);
|
|
9228
|
+
}
|
|
9229
|
+
return {
|
|
9230
|
+
uploaded,
|
|
9231
|
+
errors
|
|
9232
|
+
};
|
|
9233
|
+
}
|
|
9234
|
+
|
|
9235
|
+
// #endregion
|
|
9236
|
+
// #region published local files -- these refuse rather than pretend
|
|
9237
|
+
|
|
9238
|
+
remoteOnly(method) {
|
|
9239
|
+
throw new Error(`${method} reads a PUBLISHED file from a local directory, and this ` + `configuration uses Val's REMOTE files: a published file lives on the ` + `content host and the source carries its URL. Pending uploads are held ` + `here and do work -- see saveBase64EncodedBinaryFileFromPatch.`);
|
|
9240
|
+
}
|
|
9241
|
+
async saveBase64EncodedBinaryFileFromPatch(filePath,
|
|
9242
|
+
/*
|
|
9243
|
+
* Ignored, as in `fs` mode: the file is keyed by the patch that carries it,
|
|
9244
|
+
* so there is no parent to resolve.
|
|
9245
|
+
*/
|
|
9246
|
+
_parentRef, patchId, data, _type, metadata) {
|
|
9247
|
+
if (data === null) {
|
|
9248
|
+
// `null` records a DELETION. This is why the byte-taking sibling cannot
|
|
9249
|
+
// replace this method: there would be nothing to hand it.
|
|
9250
|
+
await this.store.deleteFile(patchId, filePath);
|
|
9251
|
+
return {
|
|
9252
|
+
patchId,
|
|
9253
|
+
filePath
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
9256
|
+
const buffer = bufferFromDataUrl(data);
|
|
9257
|
+
if (!buffer) {
|
|
9258
|
+
return {
|
|
9259
|
+
error: {
|
|
9260
|
+
message: "Could not create buffer from data url. Not a data url? First chars were: " + data.slice(0, 20)
|
|
9261
|
+
}
|
|
9262
|
+
};
|
|
9263
|
+
}
|
|
9264
|
+
await this.store.putFile({
|
|
9265
|
+
patchId,
|
|
9266
|
+
filePath,
|
|
9267
|
+
data: buffer,
|
|
9268
|
+
metadata
|
|
9269
|
+
});
|
|
9270
|
+
return {
|
|
9271
|
+
patchId,
|
|
9272
|
+
filePath
|
|
9273
|
+
};
|
|
9274
|
+
}
|
|
9275
|
+
async getBase64EncodedBinaryFileFromPatch(filePath, patchId,
|
|
9276
|
+
/*
|
|
9277
|
+
* Not consulted, and `fs` mode does not consult it either: a remote ref has
|
|
9278
|
+
* already been split by the caller, so what arrives here is the path inside
|
|
9279
|
+
* it and the pair (patchId, filePath) is the whole key.
|
|
9280
|
+
*/
|
|
9281
|
+
_remote) {
|
|
9282
|
+
var _await$this$store$get;
|
|
9283
|
+
return ((_await$this$store$get = await this.store.getFile(patchId, filePath)) === null || _await$this$store$get === void 0 ? void 0 : _await$this$store$get.data) ?? null;
|
|
9284
|
+
}
|
|
9285
|
+
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId, _remote) {
|
|
9286
|
+
const file = await this.store.getFile(patchId, filePath);
|
|
9287
|
+
if (!file || file.metadata === undefined) {
|
|
9288
|
+
return {
|
|
9289
|
+
errors: [{
|
|
9290
|
+
message: "Metadata file not found",
|
|
9291
|
+
filePath
|
|
9292
|
+
}]
|
|
9293
|
+
};
|
|
9294
|
+
}
|
|
9295
|
+
/*
|
|
9296
|
+
* Checked, not trusted. The metadata is whatever the client sent with the
|
|
9297
|
+
* upload, and a missing `width` on an image is the difference between a
|
|
9298
|
+
* layout that reserves space and one that jumps -- reported here, where the
|
|
9299
|
+
* field is named, rather than surfacing as an undefined further on.
|
|
9300
|
+
*/
|
|
9301
|
+
const fieldErrors = getFieldsForType(type).filter(field => !(field in file.metadata)).map(field => ({
|
|
9302
|
+
message: `Expected fields for type: ${type}. Field not found: '${field}'`,
|
|
9303
|
+
field
|
|
9304
|
+
}));
|
|
9305
|
+
if (fieldErrors.length > 0) {
|
|
9306
|
+
return {
|
|
9307
|
+
errors: fieldErrors
|
|
9308
|
+
};
|
|
9309
|
+
}
|
|
9310
|
+
return {
|
|
9311
|
+
metadata: file.metadata
|
|
9312
|
+
};
|
|
9313
|
+
}
|
|
9314
|
+
async getBinaryFile(_filePathOrRef) {
|
|
9315
|
+
// Null rather than a throw: callers treat this as "no such file", and a
|
|
9316
|
+
// read of a local file in a remote-files project is a miss, not a fault.
|
|
9317
|
+
return null;
|
|
9318
|
+
}
|
|
9319
|
+
async getBinaryFileMetadata(_filePath, _type) {
|
|
9320
|
+
return this.remoteOnly("getBinaryFileMetadata");
|
|
9321
|
+
}
|
|
9322
|
+
|
|
9323
|
+
// #endregion
|
|
9324
|
+
// #region no git here
|
|
9325
|
+
|
|
9326
|
+
// The same answer `ValOpsFS` gives, for the same reason and reusing its
|
|
9327
|
+
// error: history is a thing the content service holds, and there is no
|
|
9328
|
+
// repository behind this mode either. `not-supported-in-fs-mode` is the
|
|
9329
|
+
// closed union's member for exactly that, and the Studio already knows how
|
|
9330
|
+
// to show it -- a mode-specific member would be a new state to write UI for
|
|
9331
|
+
// that says the same sentence.
|
|
9332
|
+
|
|
9333
|
+
async listCommits() {
|
|
9334
|
+
return fp.result.err({
|
|
9335
|
+
kind: "not-supported-in-fs-mode"
|
|
9336
|
+
});
|
|
9337
|
+
}
|
|
9338
|
+
async getCommitPatches() {
|
|
9339
|
+
return fp.result.err({
|
|
9340
|
+
kind: "not-supported-in-fs-mode"
|
|
9341
|
+
});
|
|
9342
|
+
}
|
|
9343
|
+
async getCommitModules() {
|
|
9344
|
+
return fp.result.err({
|
|
9345
|
+
kind: "not-supported-in-fs-mode"
|
|
9346
|
+
});
|
|
9347
|
+
}
|
|
9348
|
+
async getCommitAffectedFiles() {
|
|
9349
|
+
return fp.result.err({
|
|
9350
|
+
kind: "not-supported-in-fs-mode"
|
|
9351
|
+
});
|
|
9352
|
+
}
|
|
9353
|
+
async getFileAtCommit() {
|
|
9354
|
+
return fp.result.err({
|
|
9355
|
+
kind: "not-supported-in-fs-mode"
|
|
9356
|
+
});
|
|
9357
|
+
}
|
|
9358
|
+
gitPathOfModule(
|
|
9359
|
+
// Unused: there is no repository for a module path to be relative to.
|
|
9360
|
+
_moduleFilePath) {
|
|
9361
|
+
return fp.result.err({
|
|
9362
|
+
kind: "not-supported-in-fs-mode"
|
|
9363
|
+
});
|
|
9364
|
+
}
|
|
9365
|
+
// #endregion history
|
|
9366
|
+
}
|
|
9367
|
+
|
|
9368
|
+
/** Kept exported so a host can name the error shape it may get back. */
|
|
9369
|
+
|
|
8662
9370
|
/**
|
|
8663
9371
|
* Everything that can go wrong reading, replaying or restoring history.
|
|
8664
9372
|
*
|
|
@@ -9206,15 +9914,27 @@ moduleGitPath, valTsSource, key) {
|
|
|
9206
9914
|
return fp.result.ok(path__namespace["default"].posix.join(moduleDir, entry.importPath));
|
|
9207
9915
|
}
|
|
9208
9916
|
|
|
9209
|
-
|
|
9917
|
+
/**
|
|
9918
|
+
* The fallback content host.
|
|
9919
|
+
*
|
|
9920
|
+
* Read at module scope, which is why {@link getSettings} takes an override: a
|
|
9921
|
+
* host that bundles its dependencies SEPARATELY from its own code cannot set
|
|
9922
|
+
* this. Such a build inlines env vars into the app and leaves dependency chunks
|
|
9923
|
+
* alone, so `process.env.VAL_CONTENT_URL` here is whatever it was when the
|
|
9924
|
+
* chunk was built -- usually nothing. A caller that knows the content url has
|
|
9925
|
+
* to be able to say so.
|
|
9926
|
+
*/
|
|
9927
|
+
const defaultHost$1 = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
9210
9928
|
const SettingsSchema = z.z.object({
|
|
9211
9929
|
publicProjectId: z.z.string(),
|
|
9212
9930
|
remoteFileBuckets: z.z.array(z.z.object({
|
|
9213
9931
|
bucket: z.z.string()
|
|
9214
9932
|
}))
|
|
9215
9933
|
});
|
|
9216
|
-
async function getSettings(projectName, auth
|
|
9934
|
+
async function getSettings(projectName, auth, /** Overrides {@link defaultHost}. See the note there for why this exists. */
|
|
9935
|
+
contentUrl) {
|
|
9217
9936
|
try {
|
|
9937
|
+
const host = contentUrl || defaultHost$1;
|
|
9218
9938
|
const response = await fetch(`${host}/v1/${projectName}/settings`, {
|
|
9219
9939
|
headers: "pat" in auth ? {
|
|
9220
9940
|
"x-val-pat": auth.pat,
|
|
@@ -9341,6 +10061,48 @@ const DEFAULT_VAL_BUILD_URL = "https://admin.val.build";
|
|
|
9341
10061
|
* var alone.
|
|
9342
10062
|
*/
|
|
9343
10063
|
async function initHandlerOptions(route, opts, config) {
|
|
10064
|
+
/*
|
|
10065
|
+
* A host that handed us the source has settled the question.
|
|
10066
|
+
*
|
|
10067
|
+
* First, and without consulting the environment: the other two modes are
|
|
10068
|
+
* inferred (an api key in the env is enough to make a project "proxy"), and
|
|
10069
|
+
* this one cannot be, so an env var that happens to be set must not be able
|
|
10070
|
+
* to take a host that supplied its own source and point it at a content
|
|
10071
|
+
* service instead.
|
|
10072
|
+
*/
|
|
10073
|
+
if (opts.sourceFiles !== undefined) {
|
|
10074
|
+
const valContentUrl = opts.valContentUrl || process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
10075
|
+
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || DEFAULT_VAL_BUILD_URL;
|
|
10076
|
+
/*
|
|
10077
|
+
* The same warning the other two modes get, and for the same reason.
|
|
10078
|
+
*
|
|
10079
|
+
* Returning early here skipped it, and the early return is about MODE
|
|
10080
|
+
* INFERENCE -- not about which URLs are safe. This mode still sends
|
|
10081
|
+
* `apiKey` to `valContentUrl` for remote-file settings and uploads, so a
|
|
10082
|
+
* host configured with a non-loopback `http://` content URL was putting a
|
|
10083
|
+
* credential on the wire with none of the warning fs and http modes give
|
|
10084
|
+
* for exactly that.
|
|
10085
|
+
*/
|
|
10086
|
+
warnIfInsecureUrls({
|
|
10087
|
+
valBuildUrl,
|
|
10088
|
+
valContentUrl
|
|
10089
|
+
});
|
|
10090
|
+
return {
|
|
10091
|
+
mode: "memory",
|
|
10092
|
+
route,
|
|
10093
|
+
sourceFiles: opts.sourceFiles,
|
|
10094
|
+
patchStore: opts.patchStore,
|
|
10095
|
+
unsafelyAllowUnauthenticated: opts.unsafelyAllowUnauthenticated,
|
|
10096
|
+
valContentUrl,
|
|
10097
|
+
valBuildUrl,
|
|
10098
|
+
valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
|
|
10099
|
+
valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL,
|
|
10100
|
+
apiKey: opts.apiKey || process.env.VAL_API_KEY,
|
|
10101
|
+
valSecret: opts.valSecret || process.env.VAL_SECRET,
|
|
10102
|
+
project: opts.project || process.env.VAL_PROJECT,
|
|
10103
|
+
config
|
|
10104
|
+
};
|
|
10105
|
+
}
|
|
9344
10106
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
|
9345
10107
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
|
9346
10108
|
const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
|
|
@@ -9454,6 +10216,29 @@ function createValOps(valModules, options) {
|
|
|
9454
10216
|
config: options.config
|
|
9455
10217
|
});
|
|
9456
10218
|
}
|
|
10219
|
+
if (options.mode === "memory") {
|
|
10220
|
+
/*
|
|
10221
|
+
* No backend to authenticate AGAINST, which is not the same as nothing to
|
|
10222
|
+
* authenticate. That conflation is what made this mode serve every route to
|
|
10223
|
+
* anyone who could reach the port: fs mode skips auth because it is a
|
|
10224
|
+
* developer's own machine, and this one reuses its local-store flag while
|
|
10225
|
+
* running deployed. It requires a verified session unless the host says it
|
|
10226
|
+
* has its own boundary -- see `unsafelyAllowUnauthenticated`.
|
|
10227
|
+
*
|
|
10228
|
+
* The host still holds the source and decides what a publish means; that
|
|
10229
|
+
* part is `commitPrepared` on ValServerOptions.
|
|
10230
|
+
*/
|
|
10231
|
+
return new ValOpsMemory(valModules, {
|
|
10232
|
+
formatter: options.formatter,
|
|
10233
|
+
config: options.config,
|
|
10234
|
+
sourceFiles: options.sourceFiles,
|
|
10235
|
+
patchStore: options.patchStore,
|
|
10236
|
+
unsafelyAllowUnauthenticated: options.unsafelyAllowUnauthenticated,
|
|
10237
|
+
// For pushing remote files at publish. A project with no `s.image()`
|
|
10238
|
+
// never reaches it, which is why nothing above requires it.
|
|
10239
|
+
contentUrl: options.valContentUrl
|
|
10240
|
+
});
|
|
10241
|
+
}
|
|
9457
10242
|
throw new Error(
|
|
9458
10243
|
// The union is exhausted above; this catches a config that came from
|
|
9459
10244
|
// somewhere untyped.
|
|
@@ -9572,12 +10357,23 @@ async function resolveRemoteFileAuth(options) {
|
|
|
9572
10357
|
};
|
|
9573
10358
|
}
|
|
9574
10359
|
if (options.mode !== "fs") {
|
|
9575
|
-
|
|
9576
|
-
|
|
10360
|
+
/*
|
|
10361
|
+
* `api-key-missing`, and the distinction matters to whoever reads it.
|
|
10362
|
+
*
|
|
10363
|
+
* The PAT below is read from a file in the server's own working directory,
|
|
10364
|
+
* which only `fs` mode has. Every other mode can be authenticated one way,
|
|
10365
|
+
* with an api key -- so the Studio must not offer `val login` here. It did,
|
|
10366
|
+
* because "local" used to mean "fs" and the third mode made that false: the
|
|
10367
|
+
* dialog told people to run a command, in a directory, that could not have
|
|
10368
|
+
* helped even if they found the right one.
|
|
10369
|
+
*
|
|
10370
|
+
* `project-not-configured` was also just wrong. The project may be
|
|
10371
|
+
* perfectly well configured; it is the credential that is absent.
|
|
10372
|
+
*/
|
|
9577
10373
|
return {
|
|
9578
10374
|
status: "error",
|
|
9579
|
-
errorCode: "
|
|
9580
|
-
message: "Remote
|
|
10375
|
+
errorCode: "api-key-missing",
|
|
10376
|
+
message: "Remote files need an api key here: this server cannot read a " + "personal access token, because that is a file in a working directory " + "and it has none. Set VAL_API_KEY."
|
|
9581
10377
|
};
|
|
9582
10378
|
}
|
|
9583
10379
|
// `options.cwd`, which `initHandlerOptions` sets from `process.cwd()`. The
|
|
@@ -9612,6 +10408,11 @@ async function resolveRemoteFileAuth(options) {
|
|
|
9612
10408
|
}
|
|
9613
10409
|
|
|
9614
10410
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
10411
|
+
|
|
10412
|
+
/** What {@link ValServerOptions.publishOverride} is given. */
|
|
10413
|
+
|
|
10414
|
+
/** What a publish reports back, in either shape. */
|
|
10415
|
+
|
|
9615
10416
|
const ValServer = (valModules, options, callbacks) => {
|
|
9616
10417
|
const AIContentBlock = z.z.union([z.z.object({
|
|
9617
10418
|
type: z.z.literal("text"),
|
|
@@ -9706,8 +10507,17 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
9706
10507
|
};
|
|
9707
10508
|
const getAuth = cookies => {
|
|
9708
10509
|
const cookie = cookies[internal.VAL_SESSION_COOKIE];
|
|
10510
|
+
/*
|
|
10511
|
+
* `requiresAuth`, not `patchesAreLocal`.
|
|
10512
|
+
*
|
|
10513
|
+
* These four exits return anonymous SUCCESS -- `{ error: null }` -- and all
|
|
10514
|
+
* 29 routes below treat that as authorised. That is right for fs mode,
|
|
10515
|
+
* which is a developer's own machine with no credential to require. It was
|
|
10516
|
+
* keyed on the wrong question: a local patch store is about publishing, not
|
|
10517
|
+
* about who may write, and memory mode is local AND deployed.
|
|
10518
|
+
*/
|
|
9709
10519
|
if (!options.valSecret) {
|
|
9710
|
-
if (serverOps
|
|
10520
|
+
if (!serverOps.requiresAuth) {
|
|
9711
10521
|
return {
|
|
9712
10522
|
error: null,
|
|
9713
10523
|
id: null
|
|
@@ -9721,7 +10531,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
9721
10531
|
if (typeof cookie === "string") {
|
|
9722
10532
|
const verifiedToken = verifyJwt(cookie, options.valSecret);
|
|
9723
10533
|
if (!verifiedToken.success) {
|
|
9724
|
-
if (serverOps
|
|
10534
|
+
if (!serverOps.requiresAuth) {
|
|
9725
10535
|
return {
|
|
9726
10536
|
error: null,
|
|
9727
10537
|
id: null
|
|
@@ -9733,7 +10543,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
9733
10543
|
}
|
|
9734
10544
|
const verification = IntegratedServerJwtPayload.safeParse(verifiedToken.data);
|
|
9735
10545
|
if (!verification.success) {
|
|
9736
|
-
if (serverOps
|
|
10546
|
+
if (!serverOps.requiresAuth) {
|
|
9737
10547
|
return {
|
|
9738
10548
|
error: null,
|
|
9739
10549
|
id: null
|
|
@@ -9747,7 +10557,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
9747
10557
|
id: verification.data.sub
|
|
9748
10558
|
};
|
|
9749
10559
|
} else {
|
|
9750
|
-
if (serverOps
|
|
10560
|
+
if (!serverOps.requiresAuth) {
|
|
9751
10561
|
return {
|
|
9752
10562
|
error: null,
|
|
9753
10563
|
id: null
|
|
@@ -10149,7 +10959,10 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10149
10959
|
return remoteFileAuthRes;
|
|
10150
10960
|
}
|
|
10151
10961
|
const remoteFileAuth = remoteFileAuthRes.json.remoteFileAuth;
|
|
10152
|
-
|
|
10962
|
+
|
|
10963
|
+
// The content url this server was CONFIGURED with, not whatever was in
|
|
10964
|
+
// the environment when @valbuild/server was built. See getSettings.
|
|
10965
|
+
const settingsRes = await getSettings(options.project, remoteFileAuth, options.valContentUrl);
|
|
10153
10966
|
if (!settingsRes.success) {
|
|
10154
10967
|
console.warn("Could not get remote files settings: " + settingsRes.message);
|
|
10155
10968
|
return {
|
|
@@ -10218,15 +11031,17 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10218
11031
|
json: currentStat.error
|
|
10219
11032
|
};
|
|
10220
11033
|
}
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
11034
|
+
/*
|
|
11035
|
+
* The wire value names a class and the question it answers does not.
|
|
11036
|
+
*
|
|
11037
|
+
* The client reads `mode` to decide whether it auto-saves and hides the
|
|
11038
|
+
* account panel, or whether it publishes and shows deployments -- which
|
|
11039
|
+
* is {@link ValOps.patchesAreLocal} and nothing else. It was derived by
|
|
11040
|
+
* `instanceof` while there were exactly two implementations, so a third
|
|
11041
|
+
* local store fell through to `"unknown"` and the Studio refused to
|
|
11042
|
+
* start against a server that was working.
|
|
11043
|
+
*/
|
|
11044
|
+
const mode = serverOps.patchesAreLocal ? "fs" : "http";
|
|
10230
11045
|
return {
|
|
10231
11046
|
status: 200,
|
|
10232
11047
|
json: {
|
|
@@ -10297,7 +11112,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10297
11112
|
}
|
|
10298
11113
|
};
|
|
10299
11114
|
}
|
|
10300
|
-
if (serverOps
|
|
11115
|
+
if (serverOps.patchesAreLocal) {
|
|
10301
11116
|
// In FS mode patch-file uploads are buffered through this server (no remote round-trip),
|
|
10302
11117
|
// so baseUrl points at /api/val/upload. AI image uploads, however, go straight to the
|
|
10303
11118
|
// content host — we resolve a contentBaseUrl + a PAT-issued nonce here so the browser
|
|
@@ -10307,6 +11122,19 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10307
11122
|
let contentAuthNonce = null;
|
|
10308
11123
|
if (!options.project) {
|
|
10309
11124
|
console.warn("Direct content-host uploads (AI images) disabled: no `project` set in val.config (and VAL_PROJECT env var is not set).");
|
|
11125
|
+
} else if (!(serverOps instanceof ValOpsFS)) {
|
|
11126
|
+
/*
|
|
11127
|
+
* Presigning needs a content host to presign AGAINST, and a local
|
|
11128
|
+
* store does not necessarily have one configured: `fs` mode has the
|
|
11129
|
+
* developer's `valContentUrl`, and a host holding its own source has
|
|
11130
|
+
* no such setting.
|
|
11131
|
+
*
|
|
11132
|
+
* Warned and disabled, like the missing-project case above, rather
|
|
11133
|
+
* than failing the request: everything else this route answers --
|
|
11134
|
+
* the patch upload base url -- still works, and the only thing lost
|
|
11135
|
+
* is the browser posting AI images straight to the content host.
|
|
11136
|
+
*/
|
|
11137
|
+
console.warn("Direct content-host uploads (AI images) disabled: this store has no content host to presign against.");
|
|
10310
11138
|
} else {
|
|
10311
11139
|
const authDataRes = await getRemoteFileAuth();
|
|
10312
11140
|
if (authDataRes.status !== 200) {
|
|
@@ -10388,7 +11216,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10388
11216
|
patchIds
|
|
10389
11217
|
} = req.body;
|
|
10390
11218
|
const withPatchIds = req.body.withPatchIds ?? [];
|
|
10391
|
-
if (serverOps
|
|
11219
|
+
if (serverOps.patchesAreLocal) {
|
|
10392
11220
|
return {
|
|
10393
11221
|
status: 200,
|
|
10394
11222
|
json: {
|
|
@@ -10451,7 +11279,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10451
11279
|
patchIds
|
|
10452
11280
|
} = req.body;
|
|
10453
11281
|
const withPatchIds = req.body.withPatchIds ?? [];
|
|
10454
|
-
if (serverOps
|
|
11282
|
+
if (serverOps.patchesAreLocal) {
|
|
10455
11283
|
return {
|
|
10456
11284
|
status: 200,
|
|
10457
11285
|
json: {
|
|
@@ -10530,7 +11358,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
10530
11358
|
} : {}),
|
|
10531
11359
|
withPatchIds: requestedWith ?? []
|
|
10532
11360
|
} : undefined;
|
|
10533
|
-
if (patchGroup !== undefined && serverOps
|
|
11361
|
+
if (patchGroup !== undefined && serverOps.patchesAreLocal) {
|
|
10534
11362
|
/*
|
|
10535
11363
|
* `fs` has no shared store and one author, so there is no group to
|
|
10536
11364
|
* join. Refused rather than acknowledged: answering 200 would tell the
|
|
@@ -11301,7 +12129,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11301
12129
|
}
|
|
11302
12130
|
const authDataRes = await getRemoteFileAuth();
|
|
11303
12131
|
if (authDataRes.status !== 200) {
|
|
11304
|
-
if (serverOps
|
|
12132
|
+
if (serverOps.patchesAreLocal && authDataRes.json.errorCode === "pat-error") {
|
|
11305
12133
|
return {
|
|
11306
12134
|
status: 200,
|
|
11307
12135
|
json: {
|
|
@@ -11358,7 +12186,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11358
12186
|
};
|
|
11359
12187
|
}
|
|
11360
12188
|
};
|
|
11361
|
-
if (serverOps
|
|
12189
|
+
if (serverOps.patchesAreLocal) {
|
|
11362
12190
|
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
11363
12191
|
}
|
|
11364
12192
|
if (!options.valSecret) {
|
|
@@ -11474,7 +12302,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11474
12302
|
* editing is told what was thrown away.
|
|
11475
12303
|
*/
|
|
11476
12304
|
let removed = [];
|
|
11477
|
-
if (preparedCommit.hasErrors && serverOps
|
|
12305
|
+
if (preparedCommit.hasErrors && serverOps.patchesAreLocal) {
|
|
11478
12306
|
removed = computePatchesToDrop(preparedCommit);
|
|
11479
12307
|
/*
|
|
11480
12308
|
* Nothing to drop means nothing a second `prepare` could do better.
|
|
@@ -11526,35 +12354,93 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11526
12354
|
}
|
|
11527
12355
|
};
|
|
11528
12356
|
}
|
|
11529
|
-
if (serverOps
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
12357
|
+
if (serverOps.patchesAreLocal) {
|
|
12358
|
+
/*
|
|
12359
|
+
* Writing the files is the one step that is not shared.
|
|
12360
|
+
*
|
|
12361
|
+
* Everything around it is: applying the patches, telling the ops the
|
|
12362
|
+
* sources moved, handing the result to the host, dropping the patches
|
|
12363
|
+
* this request consumed. Only the destination differs -- `fs` mode has
|
|
12364
|
+
* a working tree to write into and remote binaries to push, and a host
|
|
12365
|
+
* that holds its own source has neither. So this branch is on the
|
|
12366
|
+
* store that CAN write files rather than on "is this local", which is
|
|
12367
|
+
* what the rest of the flow asks.
|
|
12368
|
+
*/
|
|
12369
|
+
/*
|
|
12370
|
+
* Remote binary files, which publish separately from the source.
|
|
12371
|
+
*
|
|
12372
|
+
* Val uploads a remote file at PUBLISH rather than when it is added:
|
|
12373
|
+
* until then the bytes are a pending change like any other. So a
|
|
12374
|
+
* local store has to push them before the source that references them
|
|
12375
|
+
* goes live, or the new build ships a URL that 404s. `ValOpsFS` does
|
|
12376
|
+
* this inside `saveOrUploadFiles`, alongside writing its working
|
|
12377
|
+
* tree; a store with no working tree does only the push.
|
|
12378
|
+
*/
|
|
12379
|
+
if (serverOps instanceof ValOpsMemory) {
|
|
12380
|
+
const isRemoteRequired = getIsRemoteRequired(await serverOps.getSchemas());
|
|
12381
|
+
if (isRemoteRequired) {
|
|
12382
|
+
const authRes = await getRemoteFileAuth();
|
|
12383
|
+
if (authRes.status !== 200) {
|
|
12384
|
+
return authRes;
|
|
12385
|
+
}
|
|
12386
|
+
const uploadRes = await serverOps.uploadRemoteFiles(preparedCommit, authRes.json.remoteFileAuth);
|
|
12387
|
+
if (Object.keys(uploadRes.errors).length > 0) {
|
|
12388
|
+
console.error("Val: Failed to upload remote files", uploadRes.errors);
|
|
12389
|
+
return {
|
|
12390
|
+
status: 400,
|
|
12391
|
+
json: {
|
|
12392
|
+
message: "Failed to save files",
|
|
12393
|
+
details: Object.entries(uploadRes.errors).map(([ref, error]) => ({
|
|
12394
|
+
message: `Got error: ${error.message} in ${ref}`
|
|
12395
|
+
}))
|
|
12396
|
+
}
|
|
12397
|
+
};
|
|
12398
|
+
}
|
|
12399
|
+
}
|
|
11539
12400
|
}
|
|
11540
|
-
if (
|
|
11541
|
-
|
|
12401
|
+
if (serverOps instanceof ValOpsFS) {
|
|
12402
|
+
var _remoteFileAuthRes;
|
|
12403
|
+
const isRemoteRequired = getIsRemoteRequired(await serverOps.getSchemas());
|
|
12404
|
+
let mode;
|
|
12405
|
+
let remoteFileAuthRes;
|
|
12406
|
+
if (isRemoteRequired) {
|
|
12407
|
+
mode = "upload-remote";
|
|
12408
|
+
remoteFileAuthRes = await getRemoteFileAuth();
|
|
12409
|
+
} else {
|
|
12410
|
+
mode = "skip-remote";
|
|
12411
|
+
}
|
|
12412
|
+
if (remoteFileAuthRes && remoteFileAuthRes.status !== 200) {
|
|
12413
|
+
return remoteFileAuthRes;
|
|
12414
|
+
}
|
|
12415
|
+
const remoteFileAuth = (_remoteFileAuthRes = remoteFileAuthRes) === null || _remoteFileAuthRes === void 0 || (_remoteFileAuthRes = _remoteFileAuthRes.json) === null || _remoteFileAuthRes === void 0 ? void 0 : _remoteFileAuthRes.remoteFileAuth;
|
|
12416
|
+
const saveRes = await serverOps.saveOrUploadFiles(preparedCommit, mode, remoteFileAuth);
|
|
12417
|
+
if (Object.keys(saveRes.errors).length > 0) {
|
|
12418
|
+
console.error("Val: Failed to save files", saveRes.errors);
|
|
12419
|
+
return {
|
|
12420
|
+
status: 400,
|
|
12421
|
+
json: {
|
|
12422
|
+
message: "Failed to save files",
|
|
12423
|
+
details: Object.entries(saveRes.errors).map(([key, error]) => {
|
|
12424
|
+
return {
|
|
12425
|
+
message: `Got error: ${error} in ${key}`
|
|
12426
|
+
};
|
|
12427
|
+
})
|
|
12428
|
+
}
|
|
12429
|
+
};
|
|
12430
|
+
}
|
|
11542
12431
|
}
|
|
11543
|
-
|
|
11544
|
-
|
|
11545
|
-
|
|
11546
|
-
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
})
|
|
11556
|
-
}
|
|
11557
|
-
};
|
|
12432
|
+
/*
|
|
12433
|
+
* The host's destination, after the store's own.
|
|
12434
|
+
*
|
|
12435
|
+
* Ordered this way so a store that writes has already succeeded by
|
|
12436
|
+
* the time the host is told: `commitPrepared` throwing fails the save,
|
|
12437
|
+
* and a host that publishes what it was handed should not be handed
|
|
12438
|
+
* files the store could not write.
|
|
12439
|
+
*/
|
|
12440
|
+
if (options.commitPrepared) {
|
|
12441
|
+
await options.commitPrepared({
|
|
12442
|
+
patchedSourceFiles: preparedCommit.patchedSourceFiles
|
|
12443
|
+
});
|
|
11558
12444
|
}
|
|
11559
12445
|
/*
|
|
11560
12446
|
* The files on disk are the committed content now, so say so here too.
|
|
@@ -11604,7 +12490,6 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11604
12490
|
};
|
|
11605
12491
|
} else if (serverOps instanceof ValOpsHttp) {
|
|
11606
12492
|
if (auth.error === undefined && auth.id) {
|
|
11607
|
-
var _options$config$files;
|
|
11608
12493
|
/*
|
|
11609
12494
|
* The group this commit CLOSES has to be the caller's.
|
|
11610
12495
|
*
|
|
@@ -11644,14 +12529,32 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11644
12529
|
}
|
|
11645
12530
|
}
|
|
11646
12531
|
const message = body.message || "Val CMS update (" + Object.keys(analysis.patchesByModule).length + " files changed)";
|
|
11647
|
-
const
|
|
12532
|
+
const commitToGit = () => {
|
|
12533
|
+
var _options$config$files;
|
|
12534
|
+
return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val", undefined,
|
|
12535
|
+
/*
|
|
12536
|
+
* Forwarded verbatim, and only the client can decide it: the
|
|
12537
|
+
* content API closes the group it is named without checking
|
|
12538
|
+
* that the commit shipped all of it, and whether it did needs
|
|
12539
|
+
* the patch sets, which live in the browser.
|
|
12540
|
+
*/
|
|
12541
|
+
body.patchGroupId);
|
|
12542
|
+
};
|
|
11648
12543
|
/*
|
|
11649
|
-
*
|
|
11650
|
-
*
|
|
11651
|
-
*
|
|
11652
|
-
*
|
|
12544
|
+
* A host may publish somewhere other than the repository.
|
|
12545
|
+
*
|
|
12546
|
+
* See `publishOverride`. It is handed `commitToGit` rather than
|
|
12547
|
+
* having it skipped, so it can replace the commit or add to it --
|
|
12548
|
+
* and it is the host, not this route, that knows which.
|
|
11653
12549
|
*/
|
|
11654
|
-
|
|
12550
|
+
const commitRes = options.publishOverride ? await options.publishOverride({
|
|
12551
|
+
patchedSourceFiles: preparedCommit.patchedSourceFiles,
|
|
12552
|
+
preparedCommit,
|
|
12553
|
+
message,
|
|
12554
|
+
authorId: auth.id,
|
|
12555
|
+
patchGroupId: body.patchGroupId,
|
|
12556
|
+
commitToGit
|
|
12557
|
+
}) : await commitToGit();
|
|
11655
12558
|
if (commitRes.error) {
|
|
11656
12559
|
console.error("Failed to commit", commitRes.error);
|
|
11657
12560
|
if ("isNotFastForward" in commitRes && commitRes.isNotFastForward) {
|
|
@@ -11798,7 +12701,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11798
12701
|
};
|
|
11799
12702
|
}
|
|
11800
12703
|
};
|
|
11801
|
-
if (serverOps
|
|
12704
|
+
if (serverOps.patchesAreLocal) {
|
|
11802
12705
|
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
11803
12706
|
}
|
|
11804
12707
|
if (!options.valSecret) {
|
|
@@ -11902,7 +12805,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11902
12805
|
};
|
|
11903
12806
|
}
|
|
11904
12807
|
};
|
|
11905
|
-
if (serverOps
|
|
12808
|
+
if (serverOps.patchesAreLocal) {
|
|
11906
12809
|
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
11907
12810
|
}
|
|
11908
12811
|
if (!options.valSecret) {
|
|
@@ -11988,7 +12891,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
11988
12891
|
};
|
|
11989
12892
|
}
|
|
11990
12893
|
};
|
|
11991
|
-
if (serverOps
|
|
12894
|
+
if (serverOps.patchesAreLocal) {
|
|
11992
12895
|
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
11993
12896
|
}
|
|
11994
12897
|
if (!options.valSecret) {
|
|
@@ -12096,7 +12999,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12096
12999
|
};
|
|
12097
13000
|
}
|
|
12098
13001
|
};
|
|
12099
|
-
if (serverOps
|
|
13002
|
+
if (serverOps.patchesAreLocal) {
|
|
12100
13003
|
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
12101
13004
|
}
|
|
12102
13005
|
if (!options.valSecret) {
|
|
@@ -12143,7 +13046,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12143
13046
|
}
|
|
12144
13047
|
const authData = authDataRes.json.remoteFileAuth;
|
|
12145
13048
|
let headers;
|
|
12146
|
-
if (serverOps
|
|
13049
|
+
if (serverOps.patchesAreLocal) {
|
|
12147
13050
|
headers = getProfileAuthHeaders(authData, null, "application/json");
|
|
12148
13051
|
} else {
|
|
12149
13052
|
if (!("id" in auth) || !auth.id) {
|
|
@@ -12230,6 +13133,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12230
13133
|
}
|
|
12231
13134
|
};
|
|
12232
13135
|
}
|
|
13136
|
+
// Deliberately `ValOpsFS` and not `patchesAreLocal`: this writes
|
|
13137
|
+
// BYTES into a patch directory, which is a thing only the fs store
|
|
13138
|
+
// has. A local store without local binary files (ValOpsMemory, which
|
|
13139
|
+
// uses Val's remote files) skips it, and the draft image is served
|
|
13140
|
+
// from the content host rather than mirrored.
|
|
12233
13141
|
if (serverOps instanceof ValOpsFS) {
|
|
12234
13142
|
// Mirror the binaries from the content host into local patch
|
|
12235
13143
|
// storage so /files?patch_id=... can serve them. Match upstream
|
|
@@ -12334,7 +13242,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12334
13242
|
}
|
|
12335
13243
|
const authData = authDataRes.json.remoteFileAuth;
|
|
12336
13244
|
let headers;
|
|
12337
|
-
if (serverOps
|
|
13245
|
+
if (serverOps.patchesAreLocal) {
|
|
12338
13246
|
headers = getProfileAuthHeaders(authData, null, "application/json");
|
|
12339
13247
|
} else {
|
|
12340
13248
|
if (!("id" in auth) || !auth.id) {
|
|
@@ -12746,7 +13654,15 @@ chain, deleted, requested) {
|
|
|
12746
13654
|
* `undefined` means "apply everything", which is what every caller that does
|
|
12747
13655
|
* not ask for scoping gets and must keep getting.
|
|
12748
13656
|
*/
|
|
12749
|
-
async function resolveOwnPatchScope(
|
|
13657
|
+
async function resolveOwnPatchScope(
|
|
13658
|
+
/*
|
|
13659
|
+
* `ValOps`, not the two concrete stores: the one thing this needs is whether
|
|
13660
|
+
* a content service holds the groups, and it asks that below with an
|
|
13661
|
+
* `instanceof ValOpsHttp`. Naming the implementations here meant every new
|
|
13662
|
+
* store had to be added to a list to be allowed to call a function that does
|
|
13663
|
+
* not use it.
|
|
13664
|
+
*/
|
|
13665
|
+
serverOps, opts) {
|
|
12750
13666
|
let ownPatchIds;
|
|
12751
13667
|
/** See where this is set: committed work is nobody's to hold back. */
|
|
12752
13668
|
let scopeAlsoIncludesApplied = false;
|
|
@@ -13249,18 +14165,47 @@ function getIsRemoteRequired(schemas) {
|
|
|
13249
14165
|
return false;
|
|
13250
14166
|
}
|
|
13251
14167
|
|
|
13252
|
-
async function createValServer(valModules, route, opts, config, callbacks, formatter
|
|
14168
|
+
async function createValServer(valModules, route, opts, config, callbacks, formatter,
|
|
14169
|
+
/**
|
|
14170
|
+
* Called after a save has applied its patches. EXPERIMENTAL — see
|
|
14171
|
+
* `ValServerOptions.commitPrepared`.
|
|
14172
|
+
*/
|
|
14173
|
+
commitPrepared,
|
|
14174
|
+
/**
|
|
14175
|
+
* What a publish does in http mode. EXPERIMENTAL — see
|
|
14176
|
+
* `ValServerOptions.publishOverride`.
|
|
14177
|
+
*/
|
|
14178
|
+
publishOverride) {
|
|
13253
14179
|
const valServerConfig = await initHandlerOptions(route, opts, config);
|
|
13254
14180
|
return ValServer(valModules, {
|
|
13255
14181
|
formatter,
|
|
14182
|
+
commitPrepared,
|
|
14183
|
+
publishOverride,
|
|
13256
14184
|
...valServerConfig
|
|
13257
14185
|
}, callbacks);
|
|
13258
14186
|
}
|
|
13259
14187
|
|
|
13260
14188
|
// TODO: remove
|
|
14189
|
+
/**
|
|
14190
|
+
* `fs` and `path` are imported INSIDE this function, not at the top of the file.
|
|
14191
|
+
*
|
|
14192
|
+
* This is the only thing in this module that touches either, and it is a local
|
|
14193
|
+
* development convenience: scanning upwards for a `.git` to guess the commit and
|
|
14194
|
+
* branch. A static import put `fs` in the module graph of everything reaching
|
|
14195
|
+
* `createValApiRouter` -- which is every server integration, including ones that
|
|
14196
|
+
* run where there is no filesystem. Workerd provides no `fs`, so such a build
|
|
14197
|
+
* could not be bundled at all without stubbing it.
|
|
14198
|
+
*
|
|
14199
|
+
* The `await import` costs nothing here: the only caller is the CLI, on a
|
|
14200
|
+
* machine that has both.
|
|
14201
|
+
*/
|
|
13261
14202
|
async function safeReadGit(cwd) {
|
|
14203
|
+
const {
|
|
14204
|
+
promises: fs
|
|
14205
|
+
} = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
|
|
14206
|
+
const path = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('path')); });
|
|
13262
14207
|
async function findGitHead(currentDir, depth) {
|
|
13263
|
-
const gitHeadPath =
|
|
14208
|
+
const gitHeadPath = path.join(currentDir, ".git", "HEAD");
|
|
13264
14209
|
if (depth > 1000) {
|
|
13265
14210
|
console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
|
|
13266
14211
|
return {
|
|
@@ -13269,7 +14214,7 @@ async function safeReadGit(cwd) {
|
|
|
13269
14214
|
};
|
|
13270
14215
|
}
|
|
13271
14216
|
try {
|
|
13272
|
-
const headContents = await fs.
|
|
14217
|
+
const headContents = await fs.readFile(gitHeadPath, "utf-8");
|
|
13273
14218
|
const match = headContents.match(/^ref: refs\/heads\/(.+)/);
|
|
13274
14219
|
if (match) {
|
|
13275
14220
|
const branchName = match[1];
|
|
@@ -13284,7 +14229,7 @@ async function safeReadGit(cwd) {
|
|
|
13284
14229
|
};
|
|
13285
14230
|
}
|
|
13286
14231
|
} catch {
|
|
13287
|
-
const parentDir =
|
|
14232
|
+
const parentDir = path.dirname(currentDir);
|
|
13288
14233
|
|
|
13289
14234
|
// We've reached the root directory
|
|
13290
14235
|
if (parentDir === currentDir) {
|
|
@@ -13306,9 +14251,15 @@ async function safeReadGit(cwd) {
|
|
|
13306
14251
|
};
|
|
13307
14252
|
}
|
|
13308
14253
|
}
|
|
14254
|
+
|
|
14255
|
+
/** Only reached from {@link safeReadGit}; same reason for the local imports. */
|
|
13309
14256
|
async function readCommit(gitDir, branchName) {
|
|
14257
|
+
const {
|
|
14258
|
+
promises: fs
|
|
14259
|
+
} = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
|
|
14260
|
+
const path = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('path')); });
|
|
13310
14261
|
try {
|
|
13311
|
-
return (await fs.
|
|
14262
|
+
return (await fs.readFile(path.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
|
|
13312
14263
|
} catch {
|
|
13313
14264
|
return undefined;
|
|
13314
14265
|
}
|
|
@@ -15945,12 +16896,14 @@ exports.DEFAULT_LOGIN_EXPIRES_IN_SECONDS = DEFAULT_LOGIN_EXPIRES_IN_SECONDS;
|
|
|
15945
16896
|
exports.DEFAULT_LOGIN_HOST = DEFAULT_LOGIN_HOST;
|
|
15946
16897
|
exports.DEFAULT_LOGIN_POLL_INTERVAL_SECONDS = DEFAULT_LOGIN_POLL_INTERVAL_SECONDS;
|
|
15947
16898
|
exports.EXTERNAL_RESULT = EXTERNAL_RESULT;
|
|
16899
|
+
exports.InMemoryPatchStore = InMemoryPatchStore;
|
|
15948
16900
|
exports.Service = Service;
|
|
15949
16901
|
exports.ValFSHost = ValFSHost;
|
|
15950
16902
|
exports.ValLoginError = ValLoginError;
|
|
15951
16903
|
exports.ValModuleLoader = ValModuleLoader;
|
|
15952
16904
|
exports.ValOpsFS = ValOpsFS;
|
|
15953
16905
|
exports.ValOpsHttp = ValOpsHttp;
|
|
16906
|
+
exports.ValOpsMemory = ValOpsMemory;
|
|
15954
16907
|
exports.ValSourceFileHandler = ValSourceFileHandler;
|
|
15955
16908
|
exports.analyzeValModule = analyzeValModule;
|
|
15956
16909
|
exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
|