@valbuild/server 0.141.0 → 0.142.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.
@@ -2461,6 +2461,24 @@ function defineExternal(definition) {
2461
2461
  throw new Error("defineExternal is not implemented yet: phase 0 lands the contract, the registry that executes it arrives with the read endpoints.");
2462
2462
  }
2463
2463
 
2464
+ /**
2465
+ * Does saving this commit upload a file to Val's remote host?
2466
+ *
2467
+ * The question `/save` asks before it reads remote credentials. It used to ask
2468
+ * the SCHEMAS instead -- "is any of them remote?" -- and with
2469
+ * `files: { remote: true }` every media schema is, so every save needed a
2470
+ * `val login` token or an api key: a corrected typo included. Only a remote
2471
+ * FILE is uploaded, so only a commit carrying one needs credentials.
2472
+ */
2473
+ function commitCarriesRemoteFiles(preparedCommit) {
2474
+ return Object.values(preparedCommit.patchedBinaryFilesDescriptors).some(descriptor => descriptor.remote);
2475
+ }
2476
+
2477
+ /** Does this commit carry any file at all, remote or local? */
2478
+ function commitCarriesFiles(preparedCommit) {
2479
+ return Object.keys(preparedCommit.patchedBinaryFilesDescriptors).length > 0;
2480
+ }
2481
+
2464
2482
  const JwtPayloadSchema = z.object({
2465
2483
  sub: z.string(),
2466
2484
  exp: z.number(),
@@ -2661,18 +2679,41 @@ function encodeJwt(payload, sessionKey) {
2661
2679
  * error somebody had to answer instead of a default that silently applied.
2662
2680
  */
2663
2681
  function clientConfig(options) {
2682
+ const config = withResolvedProject(options.config, options.project);
2664
2683
  if (options.mode !== "http" || options.git === undefined) {
2665
- return options.config;
2684
+ return config;
2666
2685
  }
2667
- if (options.config.gitBranch !== undefined) {
2668
- return options.config;
2686
+ if (config.gitBranch !== undefined) {
2687
+ return config;
2669
2688
  }
2670
2689
  return {
2671
- ...options.config,
2690
+ ...config,
2672
2691
  gitBranch: options.git.branch
2673
2692
  };
2674
2693
  }
2675
2694
 
2695
+ /**
2696
+ * The project, filled in the same way and for the same reason as the branch.
2697
+ *
2698
+ * `project` may come from `VAL_PROJECT` rather than from `val.config`, and the
2699
+ * server talks to that project either way. The Studio decides from
2700
+ * `config.project` whether there is an assistant to offer at all (`wsEnabled`
2701
+ * in `ValProvider`), so a project named only in the environment would have
2702
+ * had it hidden while the server behind it was ready to serve it.
2703
+ *
2704
+ * `val.config` wins, as it does for the branch, and every mode gets it: unlike
2705
+ * a branch, a project means the same thing in fs mode as in http mode.
2706
+ */
2707
+ function withResolvedProject(config, project) {
2708
+ if (config.project !== undefined || project === undefined) {
2709
+ return config;
2710
+ }
2711
+ return {
2712
+ ...config,
2713
+ project
2714
+ };
2715
+ }
2716
+
2676
2717
  /**
2677
2718
  * Which IP addresses this server may open a connection to.
2678
2719
  *
@@ -3546,6 +3587,12 @@ class ValOps {
3546
3587
  * hash. Re-folding with the wrong list changes the base SHA for no reason.
3547
3588
  */
3548
3589
  shaModuleErrors;
3590
+ /**
3591
+ * The SITE's Source for the modules a proposal's snapshot replaced: what the
3592
+ * bundle evaluated to before the snapshot went over it. Kept for
3593
+ * {@link siteSourcesUnderSnapshot}, and only where there is a snapshot.
3594
+ */
3595
+ siteSourcesBeforeSnapshot = null;
3549
3596
  /**
3550
3597
  * What a save has told us each `.jsonValues()` entry now holds.
3551
3598
  *
@@ -3592,6 +3639,7 @@ class ValOps {
3592
3639
  // #region initTree
3593
3640
  async initSources() {
3594
3641
  if (this.baseSha === null || this.sourcesSha === null || this.configSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
3642
+ var _this$options;
3595
3643
  const extracted = await extractValModules(this.valModules);
3596
3644
  const moduleErrors = extracted.moduleErrors.concat(findNestedJsonValuesModuleErrors(extracted.schemas));
3597
3645
  this.sources = extracted.sources;
@@ -3603,12 +3651,29 @@ class ValOps {
3603
3651
  this.modulesErrors = moduleErrors;
3604
3652
  this.shaEntries = extracted.shaEntries;
3605
3653
  this.shaModuleErrors = extracted.moduleErrors;
3654
+ /*
3655
+ * The snapshot, over what the modules evaluated to. Through the same
3656
+ * door a save uses, so the SHAs move with it: they describe the Source
3657
+ * being served, and a base SHA that still described the bundle would
3658
+ * say the snapshot's content was the bundle's.
3659
+ */
3660
+ if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.snapshotSources) {
3661
+ const site = {};
3662
+ for (const moduleFilePath of Object.keys(this.options.snapshotSources)) {
3663
+ const source = extracted.sources[moduleFilePath];
3664
+ if (source !== undefined) {
3665
+ site[moduleFilePath] = source;
3666
+ }
3667
+ }
3668
+ this.siteSourcesBeforeSnapshot = site;
3669
+ this.promoteCommittedSources(this.options.snapshotSources);
3670
+ }
3606
3671
  return {
3607
3672
  baseSha: this.baseSha,
3608
3673
  schemaSha: this.schemaSha,
3609
3674
  sourcesSha: this.sourcesSha,
3610
3675
  configSha: this.configSha,
3611
- sources: extracted.sources,
3676
+ sources: this.sources,
3612
3677
  schemas: extracted.schemas,
3613
3678
  moduleErrors
3614
3679
  };
@@ -3624,6 +3689,19 @@ class ValOps {
3624
3689
  };
3625
3690
  }
3626
3691
 
3692
+ /**
3693
+ * In a proposal: the SITE's Source for every module the proposal has saved,
3694
+ * as the build it is based on evaluated it -- what the proposal is compared
3695
+ * against, and what merging it changes. Every other module is the same in
3696
+ * both, so it is not sent. `null` anywhere that is not a proposal.
3697
+ */
3698
+ async siteSourcesUnderSnapshot() {
3699
+ var _this$options2;
3700
+ if (!((_this$options2 = this.options) !== null && _this$options2 !== void 0 && _this$options2.snapshotSources)) return null;
3701
+ await this.initSources();
3702
+ return this.siteSourcesBeforeSnapshot;
3703
+ }
3704
+
3627
3705
  /**
3628
3706
  * These patches are on disk now: adopt what they produced as the committed
3629
3707
  * sources.
@@ -5220,9 +5298,9 @@ class ValOps {
5220
5298
  // https://github.com/microsoft/TypeScript/issues/36174
5221
5299
  let sourceFileText = null;
5222
5300
  if (tsChanged) {
5223
- var _this$options;
5301
+ var _this$options3;
5224
5302
  sourceFileText = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
5225
- if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.formatter) {
5303
+ if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.formatter) {
5226
5304
  try {
5227
5305
  sourceFileText = await this.options.formatter(sourceFileText, path);
5228
5306
  } catch (err) {
@@ -5234,13 +5312,13 @@ class ValOps {
5234
5312
  }
5235
5313
  const extraFiles = {};
5236
5314
  for (const [jsonPath, content] of Array.from(jsonEntryContents)) {
5237
- var _this$options2;
5315
+ var _this$options4;
5238
5316
  if (content === null) {
5239
5317
  extraFiles[jsonPath] = null;
5240
5318
  continue;
5241
5319
  }
5242
5320
  let jsonText = JSON.stringify(content, null, 2);
5243
- if ((_this$options2 = this.options) !== null && _this$options2 !== void 0 && _this$options2.formatter) {
5321
+ if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.formatter) {
5244
5322
  try {
5245
5323
  jsonText = await this.options.formatter(jsonText, jsonPath);
5246
5324
  } catch (err) {
@@ -5556,6 +5634,30 @@ class ValOps {
5556
5634
  };
5557
5635
  }
5558
5636
 
5637
+ /**
5638
+ * The proposal this server runs at the address of, or `null` on the site --
5639
+ * and always in `fs` and memory mode, which have no proposals. See
5640
+ * `ValProposal`.
5641
+ */
5642
+ currentProposal() {
5643
+ return null;
5644
+ }
5645
+
5646
+ /**
5647
+ * The content service's proposals API, for the Studio's switcher, list and
5648
+ * dialogs. Refused here for the reason {@link publishApi} is: there is no
5649
+ * content service to forward to. `profileId` is the person asking.
5650
+ */
5651
+ async proposalsApi(_path, _init, _profileId) {
5652
+ return {
5653
+ status: 501,
5654
+ contentType: "application/json",
5655
+ body: JSON.stringify({
5656
+ message: "This Val server has no content service, so it has no proposals."
5657
+ })
5658
+ };
5659
+ }
5660
+
5559
5661
  /**
5560
5662
  * Whether a commit here produces `.val.ts` TEXT as well as data.
5561
5663
  *
@@ -8759,6 +8861,111 @@ function computePatchesToDrop(prepared) {
8759
8861
  return dropped;
8760
8862
  }
8761
8863
 
8864
+ /**
8865
+ * A proposal this server runs at the address of: an editor-managed branch of
8866
+ * the project's content, served from its own copy of the content the
8867
+ * proposal has saved -- the **snapshot**. valbuild/home `docs/proposals.md`,
8868
+ * "Saving".
8869
+ *
8870
+ * A third place Source can come from, beside the two Val already has:
8871
+ *
8872
+ * | source provider | Source comes from |
8873
+ * | --------------- | -------------------------------------- |
8874
+ * | committed | the bundle |
8875
+ * | draft | content, pending patches applied |
8876
+ * | snapshot | the proposal's saves |
8877
+ *
8878
+ * The snapshot REPLACES the bundle's Source for the modules the proposal has
8879
+ * saved and nothing else, on every read: a committed render, the draft a page
8880
+ * starts from, the Studio's tree, validation and the next save's prepare. The
8881
+ * proposal's pending patches apply on top of it, as the site's apply on top of
8882
+ * the bundle.
8883
+ *
8884
+ * Handed over by the platform that serves the address, per isolate: the
8885
+ * snapshot only changes with a save, and a save is a new isolate.
8886
+ */
8887
+
8888
+ /**
8889
+ * The proposal a platform describes in its runtime env, or `undefined` when
8890
+ * this is not a proposal's address.
8891
+ *
8892
+ * `VAL_PROPOSAL` and `VAL_BRANCH` name it; `VAL_OVERLAY` is the snapshot as
8893
+ * JSON (`{ commit, modules, files }`). Throws on a snapshot that does not
8894
+ * parse rather than serving the base build as though it were the proposal: a
8895
+ * reviewer would be looking at the wrong content and nothing would say so.
8896
+ */
8897
+ function proposalFromEnv(env) {
8898
+ const name = env.VAL_PROPOSAL;
8899
+ const branch = env.VAL_BRANCH;
8900
+ if (!name || !branch) {
8901
+ return undefined;
8902
+ }
8903
+ let parsed = {};
8904
+ if (env.VAL_OVERLAY) {
8905
+ try {
8906
+ parsed = JSON.parse(env.VAL_OVERLAY);
8907
+ } catch (err) {
8908
+ throw new Error(`Val: proposal ${name}: VAL_OVERLAY is not JSON (${err instanceof Error ? err.message : String(err)})`, {
8909
+ cause: err
8910
+ });
8911
+ }
8912
+ }
8913
+ if (!isObject$1(parsed)) {
8914
+ throw new Error(`Val: proposal ${name}: VAL_OVERLAY is not an object`);
8915
+ }
8916
+ const modulesIn = parsed["modules"] ?? {};
8917
+ const filesIn = parsed["files"] ?? {};
8918
+ const commit = parsed["commit"] ?? null;
8919
+ if (!isObject$1(modulesIn) || !isObject$1(filesIn)) {
8920
+ throw new Error(`Val: proposal ${name}: VAL_OVERLAY's modules and files must be objects`);
8921
+ }
8922
+ if (commit !== null && typeof commit !== "string") {
8923
+ throw new Error(`Val: proposal ${name}: VAL_OVERLAY's commit must be a string or null`);
8924
+ }
8925
+ const files = {};
8926
+ for (const [path, text] of Object.entries(filesIn)) {
8927
+ if (typeof text === "string") {
8928
+ files[path] = text;
8929
+ }
8930
+ }
8931
+ return {
8932
+ name,
8933
+ branch,
8934
+ commit,
8935
+ modules: modulesIn,
8936
+ files
8937
+ };
8938
+ }
8939
+
8940
+ /**
8941
+ * The proposal's saved Source, as Source: every module whose value is JSON.
8942
+ * One place, so the server and the readers that render without it agree on
8943
+ * what the snapshot holds.
8944
+ */
8945
+ function proposalSnapshot(proposal) {
8946
+ const snapshot = {};
8947
+ for (const [path, source] of Object.entries(proposal.modules)) {
8948
+ if (isJsonValue(source)) {
8949
+ snapshot[path] = source;
8950
+ }
8951
+ }
8952
+ return snapshot;
8953
+ }
8954
+ function isObject$1(value) {
8955
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8956
+ }
8957
+
8958
+ /** What `JSON.parse` can produce, checked rather than assumed. */
8959
+ function isJsonValue(value) {
8960
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
8961
+ return true;
8962
+ }
8963
+ if (Array.isArray(value)) {
8964
+ return value.every(isJsonValue);
8965
+ }
8966
+ return isObject$1(value) && Object.values(value).every(isJsonValue);
8967
+ }
8968
+
8762
8969
  const textEncoder$2 = new TextEncoder();
8763
8970
  const PatchId = z.string().refine(s => !!s); // TODO: validate
8764
8971
  const CommitSha = z.string().refine(s => !!s); // TODO: validate
@@ -9112,6 +9319,8 @@ class ValOpsHttp extends ValOps {
9112
9319
  publishJob;
9113
9320
  /** See `publishBuild` on {@link ValApiOptions}; sent with every position. */
9114
9321
  publishBuild;
9322
+ /** See `proposal` on {@link ValApiOptions}; `null` on the site. */
9323
+ proposal;
9115
9324
 
9116
9325
  /** Did the host hand over the running build's source? See `projectSource`. */
9117
9326
  embedsSource() {
@@ -9184,7 +9393,10 @@ class ValOpsHttp extends ValOps {
9184
9393
  * getSettings / uploadRemoteFile / getPresignedAuthNonce.
9185
9394
  */
9186
9395
  auth, valModules, options) {
9187
- super(valModules, options);
9396
+ super(valModules, options !== null && options !== void 0 && options.proposal ? {
9397
+ ...options,
9398
+ snapshotSources: proposalSnapshot(options.proposal)
9399
+ } : options);
9188
9400
  this.contentUrl = contentUrl;
9189
9401
  this.project = project;
9190
9402
  this.git = git;
@@ -9194,7 +9406,15 @@ class ValOpsHttp extends ValOps {
9194
9406
  Authorization: `Bearer ${auth.apiKey}`
9195
9407
  };
9196
9408
  this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
9197
- this.projectSource = (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
9409
+ this.proposal = (options === null || options === void 0 ? void 0 : options.proposal) ?? null;
9410
+ /*
9411
+ * In a proposal, the text its saves wrote over the text the build was made
9412
+ * from: the next save patches the last one's `.val.ts`, not the base's.
9413
+ */
9414
+ this.projectSource = this.proposal !== null ? {
9415
+ ...((options === null || options === void 0 ? void 0 : options.projectSource) ?? {}),
9416
+ ...this.proposal.files
9417
+ } : (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
9198
9418
  this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
9199
9419
  this.publishBuild = (options === null || options === void 0 ? void 0 : options.publishBuild) ?? null;
9200
9420
  this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
@@ -9223,7 +9443,8 @@ class ValOpsHttp extends ValOps {
9223
9443
  */
9224
9444
  publishRefusal() {
9225
9445
  var _this$projectExpectat;
9226
- if (this.git !== null) {
9446
+ // A save in a proposal mirrors nothing: it reaches git by merging.
9447
+ if (this.git !== null || this.proposal !== null) {
9227
9448
  return null;
9228
9449
  }
9229
9450
  /*
@@ -9260,6 +9481,13 @@ class ValOpsHttp extends ValOps {
9260
9481
  */
9261
9482
  sourceMode() {
9262
9483
  var _this$projectExpectat2;
9484
+ /*
9485
+ * Not reported in a proposal, whatever the project is. The Studio reads
9486
+ * `managed` as "I am the deployer" and follows a commit with a build it
9487
+ * publishes to the SITE -- and a proposal's commit is a Save, which its
9488
+ * address serves without any build at all.
9489
+ */
9490
+ if (this.proposal !== null) return null;
9263
9491
  return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
9264
9492
  }
9265
9493
 
@@ -9294,6 +9522,8 @@ class ValOpsHttp extends ValOps {
9294
9522
  }).catch(() => undefined);
9295
9523
  }
9296
9524
  publishesAsJobs() {
9525
+ // A proposal's Save is a commit on its branch; its merge is the publish.
9526
+ if (this.proposal !== null) return false;
9297
9527
  const expected = this.projectExpectation;
9298
9528
  if (expected === null) return false;
9299
9529
  return expected.publishJobs ?? expected.sourceMode === "managed";
@@ -9413,6 +9643,10 @@ class ValOpsHttp extends ValOps {
9413
9643
  * segment gets through.
9414
9644
  */
9415
9645
  static publishApiPathAllowed(path) {
9646
+ // A merge's prepare is content's own, from the proposal's last save.
9647
+ if (/^\/publish-jobs\/[A-Za-z0-9_-]{1,100}\/merge-prepare$/.test(path)) {
9648
+ return true;
9649
+ }
9416
9650
  if (path === "/build-target" || path === "/project-source" || path === "/update-target" || path === "/publish" ||
9417
9651
  // Where "View run" goes, for a connected build CI reported failed.
9418
9652
  // Read only: reporting a run is CI's, with its own token.
@@ -9435,6 +9669,20 @@ class ValOpsHttp extends ValOps {
9435
9669
  return /^\/publish\/[A-Za-z0-9_-]+(\/(artifacts|verify|promote))?$/.test(path);
9436
9670
  }
9437
9671
  async publishApi(path, init) {
9672
+ /*
9673
+ * Nothing is published from a proposal's address: its way to the site is
9674
+ * a merge. The publish API reaches the SITE's builds and jobs, so a
9675
+ * Studio there that asked would be publishing over the site.
9676
+ */
9677
+ if (this.proposal !== null && !ValOpsHttp.mergeApiPathAllowed(init.method, path)) {
9678
+ return {
9679
+ status: 403,
9680
+ contentType: "application/json",
9681
+ body: JSON.stringify({
9682
+ message: "This is a proposal's address: nothing is published from here. " + "A proposal reaches the site by being merged."
9683
+ })
9684
+ };
9685
+ }
9438
9686
  if (!ValOpsHttp.publishApiPathAllowed(path)) {
9439
9687
  return {
9440
9688
  status: 403,
@@ -9446,6 +9694,82 @@ class ValOpsHttp extends ValOps {
9446
9694
  }
9447
9695
  return this.callPublishApi(path, init);
9448
9696
  }
9697
+ currentProposal() {
9698
+ return this.proposal === null ? null : {
9699
+ name: this.proposal.name,
9700
+ branch: this.proposal.branch
9701
+ };
9702
+ }
9703
+
9704
+ /**
9705
+ * Which proposal calls the browser may make, by method and the path after
9706
+ * `/proposals`: list and open, read and rename one, and close, reopen and
9707
+ * retry its setup. Not `/save` -- the Studio saves through `/save`, which
9708
+ * prepares the commit here -- and not `/patches`, which moves the site's
9709
+ * changes into a proposal and has no screen yet.
9710
+ */
9711
+ static proposalsApiAllowed(method, path) {
9712
+ if (path === "") return method === "GET" || method === "POST";
9713
+ const one = /^\/[0-9a-f]{20}$/.test(path);
9714
+ if (one) return method === "GET" || method === "PATCH";
9715
+ if (method === "GET") return /^\/[0-9a-f]{20}\/merge-checks$/.test(path);
9716
+ return method === "POST" && /^\/[0-9a-f]{20}\/(close|reopen|setup\/retry|merge)$/.test(path);
9717
+ }
9718
+
9719
+ /**
9720
+ * What a proposal's address may reach of the publish API: building ITS OWN
9721
+ * merge, and nothing that publishes the site's changes. The merge's prepare
9722
+ * (content's, from the proposal's last save), the build it needs -- the
9723
+ * site's build target and source, declaring and uploading the build -- and
9724
+ * the job's own steps. Not a press of the site's Publish, not `next` (which
9725
+ * would hand this tab a site job), and not the site job's prepare.
9726
+ */
9727
+ static mergeApiPathAllowed(method, path) {
9728
+ if (path === "/build-target" || path === "/project-source" || path === "/publish") {
9729
+ return true;
9730
+ }
9731
+ if (/^\/publish\/[A-Za-z0-9_-]+\/artifacts$/.test(path)) return true;
9732
+ // Where its press is -- read only: a POST there is the site's Try again.
9733
+ if (method === "GET" && path !== "/publish-requests/try-again" && /^\/publish-requests\/[A-Za-z0-9_-]{1,100}$/.test(path)) return true;
9734
+ return /^\/publish-jobs\/[A-Za-z0-9_-]{1,100}\/(merge-prepare|steps|renew|cancel)$/.test(path);
9735
+ }
9736
+ async proposalsApi(path, init, profileId) {
9737
+ const json = "application/json";
9738
+ if (!ValOpsHttp.proposalsApiAllowed(init.method, path)) {
9739
+ return {
9740
+ status: 403,
9741
+ contentType: json,
9742
+ body: JSON.stringify({
9743
+ message: `'${init.method} ${path}' is not part of the proposals API.`
9744
+ })
9745
+ };
9746
+ }
9747
+ const res = await fetch(`${this.contentUrl}/v1/${this.project}/proposals${path}`, {
9748
+ method: init.method,
9749
+ headers: {
9750
+ ...this.authHeaders,
9751
+ /*
9752
+ * WHO is asking, as for a commit: the app's key names the project,
9753
+ * and a proposal is opened, renamed and closed by a person -- content
9754
+ * refuses those writes from a credential that names nobody.
9755
+ */
9756
+ ...(profileId !== null ? {
9757
+ "x-val-profile-id": profileId
9758
+ } : {}),
9759
+ ...(init.body === undefined ? {} : {
9760
+ "Content-Type": json
9761
+ })
9762
+ },
9763
+ ...(init.body === undefined ? {} : {
9764
+ body: init.body
9765
+ })
9766
+ });
9767
+ return {
9768
+ status: res.status,
9769
+ body: await res.text(),
9770
+ contentType: res.headers.get("Content-Type") ?? json
9771
+ };
9772
+ }
9449
9773
 
9450
9774
  /**
9451
9775
  * A publish job's prepare, sent to content: the job's sources, archived
@@ -9746,6 +10070,42 @@ class ValOpsHttp extends ValOps {
9746
10070
  } : {})
9747
10071
  };
9748
10072
  }
10073
+
10074
+ /**
10075
+ * Where this server sits in the content service's chain, as every request
10076
+ * that asks about a position says it.
10077
+ *
10078
+ * In a proposal: its branch, and the save its snapshot is (none before the
10079
+ * first). The content service then answers with what came after that save
10080
+ * and nothing else -- never the site's position, which would replay the
10081
+ * proposal's own saves over a snapshot that already holds them. Otherwise
10082
+ * the commit this build was made from, or, for a build a tab made, its job.
10083
+ */
10084
+ position() {
10085
+ if (this.proposal !== null) {
10086
+ return {
10087
+ branch: this.proposal.branch,
10088
+ ...(this.proposal.commit !== null ? {
10089
+ commit: this.proposal.commit
10090
+ } : {})
10091
+ };
10092
+ }
10093
+ if (this.git) {
10094
+ return {
10095
+ branch: this.git.branch,
10096
+ commit: this.git.commit
10097
+ };
10098
+ }
10099
+ return this.publishJob !== null ? {
10100
+ job: this.publishJob
10101
+ } : {};
10102
+ }
10103
+
10104
+ /** The branch this server's patches are on, where it knows it. */
10105
+ ownBranch() {
10106
+ var _this$proposal, _this$git;
10107
+ return ((_this$proposal = this.proposal) === null || _this$proposal === void 0 ? void 0 : _this$proposal.branch) ?? ((_this$git = this.git) === null || _this$git === void 0 ? void 0 : _this$git.branch);
10108
+ }
9749
10109
  async getWebSocketNonce(profileId) {
9750
10110
  return fetch(`${this.contentUrl}/v1/${this.project}/websocket/nonces`, {
9751
10111
  method: "POST",
@@ -9757,12 +10117,24 @@ class ValOpsHttp extends ValOps {
9757
10117
  * the project -- and a nonce is scoped to the project and the person,
9758
10118
  * not to a position in a chain.
9759
10119
  */
9760
- ...(this.git ? {
9761
- branch: this.git.branch,
9762
- commitSha: this.git.commit
9763
- } : this.publishJob !== null ? {
9764
- job: this.publishJob
9765
- } : {}),
10120
+ ...(() => {
10121
+ const {
10122
+ branch,
10123
+ commit,
10124
+ job
10125
+ } = this.position();
10126
+ return {
10127
+ ...(branch !== undefined ? {
10128
+ branch
10129
+ } : {}),
10130
+ ...(commit !== undefined ? {
10131
+ commitSha: commit
10132
+ } : {}),
10133
+ ...(job !== undefined ? {
10134
+ job
10135
+ } : {})
10136
+ };
10137
+ })(),
9766
10138
  // Which build is asking, beside what it says about itself.
9767
10139
  ...(this.publishBuild !== null ? {
9768
10140
  build: this.publishBuild
@@ -9944,12 +10316,10 @@ class ValOpsHttp extends ValOps {
9944
10316
  * holds the chain, and for a project it is the only publisher of, the
9945
10317
  * position IS the head.
9946
10318
  */
9947
- if (this.git) {
9948
- params.push(["branch", this.git.branch]);
9949
- params.push(["commit", this.git.commit]);
9950
- } else if (this.publishJob !== null) {
9951
- // A build a tab made has no commit, and says which job it was made for.
9952
- params.push(["job", this.publishJob]);
10319
+ // A build a tab made has no commit, and says which job it was made for;
10320
+ // a proposal says its branch and its save. See `position`.
10321
+ for (const [key, value] of Object.entries(this.position())) {
10322
+ if (value !== undefined) params.push([key, value]);
9953
10323
  }
9954
10324
  /*
9955
10325
  * And WHICH build this is, when the platform running it says: the only
@@ -10222,6 +10592,23 @@ class ValOpsHttp extends ValOps {
10222
10592
  * cache: a second render a second later asks again.
10223
10593
  */
10224
10594
  patchGroupsCache = null;
10595
+
10596
+ /**
10597
+ * Forget the remembered group list, after a write through this server.
10598
+ *
10599
+ * A write changes membership -- a save joins its author's group, a delete or
10600
+ * an unstage leaves it -- and the window above is long enough for the next
10601
+ * render to be served the list from before it: an editor who saved and
10602
+ * reloaded inside a second was shown their draft without the change they had
10603
+ * just made. Called once the write has answered, so a read that started
10604
+ * while it was in flight cannot leave the old list behind it.
10605
+ *
10606
+ * Only this instance's writes. Another instance's write is still seen at
10607
+ * most one window late, which is what the window is for.
10608
+ */
10609
+ forgetPatchGroups() {
10610
+ this.patchGroupsCache = null;
10611
+ }
10225
10612
  async getPatchGroups(options) {
10226
10613
  const now = Date.now();
10227
10614
  if ((options === null || options === void 0 ? void 0 : options.fresh) !== true && this.patchGroupsCache !== null && now - this.patchGroupsCache.at < PATCH_GROUPS_CACHE_MS) {
@@ -10253,6 +10640,17 @@ class ValOpsHttp extends ValOps {
10253
10640
  return res;
10254
10641
  }
10255
10642
  async fetchPatchGroups() {
10643
+ /*
10644
+ * A proposal has no patch groups: its changes are one unit, saved
10645
+ * together, and the content service answers 404 for groups on its branch.
10646
+ * "Unsupported" is exactly that answer, and it is what makes a draft in a
10647
+ * proposal show all of the proposal's changes rather than one person's.
10648
+ */
10649
+ if (this.proposal !== null) {
10650
+ return {
10651
+ status: "unsupported"
10652
+ };
10653
+ }
10256
10654
  try {
10257
10655
  /*
10258
10656
  * `branch` is REQUIRED by the endpoint, which answers 400 without it.
@@ -10261,7 +10659,8 @@ class ValOpsHttp extends ValOps {
10261
10659
  * `saveSourceFilePatch`): groups are per branch, so a request without one
10262
10660
  * is not merely under-specified, it is rejected.
10263
10661
  */
10264
- const params = new URLSearchParams(this.git ? [["branch", this.git.branch]] : []);
10662
+ const branch = this.ownBranch();
10663
+ const params = new URLSearchParams(branch !== undefined ? [["branch", branch]] : []);
10265
10664
  const res = await fetch(`${this.contentUrl}/v1/${this.project}/patch-groups?${params}`, {
10266
10665
  headers: this.authHeaders
10267
10666
  });
@@ -10312,8 +10711,9 @@ class ValOpsHttp extends ValOps {
10312
10711
  * Without one the content API uses the project's own, as it does for reads.
10313
10712
  */
10314
10713
  ownGroupBranch() {
10315
- return this.git ? {
10316
- branch: this.git.branch
10714
+ const branch = this.ownBranch();
10715
+ return branch !== undefined ? {
10716
+ branch
10317
10717
  } : {};
10318
10718
  }
10319
10719
  async mutatePatchGroup(method, path, body,
@@ -10336,6 +10736,22 @@ class ValOpsHttp extends ValOps {
10336
10736
  * on its own there and this adds nothing.
10337
10737
  */
10338
10738
  authorId) {
10739
+ if (this.proposal !== null) {
10740
+ return {
10741
+ status: 409,
10742
+ patchIds: [],
10743
+ error: {
10744
+ message: "A proposal's changes are saved together, so there is nothing to stage or unstage in one."
10745
+ }
10746
+ };
10747
+ }
10748
+ try {
10749
+ return await this.mutatePatchGroupRequest(method, path, body, authorId);
10750
+ } finally {
10751
+ this.forgetPatchGroups();
10752
+ }
10753
+ }
10754
+ async mutatePatchGroupRequest(method, path, body, authorId) {
10339
10755
  try {
10340
10756
  const res = await fetch(`${this.contentUrl}/v1/${this.project}/${path}`, {
10341
10757
  method,
@@ -10428,7 +10844,13 @@ class ValOpsHttp extends ValOps {
10428
10844
  patchId,
10429
10845
  parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
10430
10846
  baseSha,
10431
- ...(this.git ? {
10847
+ /*
10848
+ * In a proposal, its branch alone: the content service files the
10849
+ * patch there, and only for an open proposal.
10850
+ */
10851
+ ...(this.proposal !== null ? {
10852
+ branch: this.proposal.branch
10853
+ } : this.git ? {
10432
10854
  commit: this.git.commit,
10433
10855
  branch: this.git.branch
10434
10856
  } : {}),
@@ -10505,7 +10927,7 @@ class ValOpsHttp extends ValOps {
10505
10927
  errorType: "other",
10506
10928
  message: `Could save source file patch (connection error?): ${e instanceof Error ? e.message : e.toString()}`
10507
10929
  });
10508
- });
10930
+ }).finally(() => this.forgetPatchGroups());
10509
10931
  }
10510
10932
 
10511
10933
  /**
@@ -10797,12 +11219,11 @@ class ValOpsHttp extends ValOps {
10797
11219
  if (res.ok) {
10798
11220
  const parsed = DeletePatchesResponse.safeParse(await res.json());
10799
11221
  if (parsed.success) {
10800
- var _this$git;
10801
11222
  const errors = {};
10802
11223
  for (const err of parsed.data.errors || []) {
10803
11224
  errors[err.patchId] = err;
10804
11225
  }
10805
- const headVersion = ownBranchVersion(parsed.data.headVersions, (_this$git = this.git) === null || _this$git === void 0 ? void 0 : _this$git.branch);
11226
+ const headVersion = ownBranchVersion(parsed.data.headVersions, this.ownBranch());
10806
11227
  const version = headVersion !== undefined ? {
10807
11228
  headVersion
10808
11229
  } : {};
@@ -10835,7 +11256,7 @@ class ValOpsHttp extends ValOps {
10835
11256
  message: `Could not delete patches (connection error?): ${e instanceof Error ? e.message : e.toString()}`
10836
11257
  }
10837
11258
  };
10838
- });
11259
+ }).finally(() => this.forgetPatchGroups());
10839
11260
  }
10840
11261
  async commit(prepared, message, committer, filesDirectory, newBranch,
10841
11262
  /**
@@ -10855,7 +11276,13 @@ class ValOpsHttp extends ValOps {
10855
11276
  patchGroupId) {
10856
11277
  try {
10857
11278
  var _res$headers$get3;
10858
- const res = await fetch(`${this.contentUrl}/v1/${this.project}/commit`, {
11279
+ /*
11280
+ * A save in a proposal is a commit on the proposal's branch, at its own
11281
+ * route: the site's `/commit` refuses a proposal's patches, which is
11282
+ * the line that keeps them off the site until a merge.
11283
+ */
11284
+ const commitUrl = this.proposal !== null ? `${this.contentUrl}/v1/${this.project}/proposals/${this.proposal.name}/save` : `${this.contentUrl}/v1/${this.project}/commit`;
11285
+ const res = await fetch(commitUrl, {
10859
11286
  method: "POST",
10860
11287
  headers: {
10861
11288
  ...this.authHeaders,
@@ -12920,6 +13347,9 @@ async function initHandlerOptions(route, opts, config) {
12920
13347
  ...(opts.publishBuild !== undefined ? {
12921
13348
  publishBuild: opts.publishBuild
12922
13349
  } : {}),
13350
+ ...(opts.proposal !== undefined ? {
13351
+ proposal: opts.proposal
13352
+ } : {}),
12923
13353
  project: maybeValProject,
12924
13354
  valEnableRedirectUrl,
12925
13355
  valDisableRedirectUrl,
@@ -12995,6 +13425,9 @@ function createValOps(valModules, options) {
12995
13425
  } : {}),
12996
13426
  ...(options.publishBuild !== undefined ? {
12997
13427
  publishBuild: options.publishBuild
13428
+ } : {}),
13429
+ ...(options.proposal !== undefined ? {
13430
+ proposal: options.proposal
12998
13431
  } : {})
12999
13432
  });
13000
13433
  }
@@ -13593,6 +14026,32 @@ const ValServer = (valModules, options, callbacks) => {
13593
14026
  };
13594
14027
  }
13595
14028
  };
14029
+
14030
+ /** The person a session names, or `null` for an anonymous one. */
14031
+ const profileOf = auth => "id" in auth && auth.id ? auth.id : null;
14032
+
14033
+ /** One proposals-API call, carried as `proxyPublishApi` carries one. */
14034
+ const proxyProposalsApi = async (path, method, profileId, body) => {
14035
+ const answer = await serverOps.proposalsApi(path ?? "", {
14036
+ method,
14037
+ ...(body === undefined ? {} : {
14038
+ body
14039
+ })
14040
+ }, profileId);
14041
+ try {
14042
+ return {
14043
+ status: answer.status,
14044
+ json: JSON.parse(answer.body)
14045
+ };
14046
+ } catch {
14047
+ return {
14048
+ status: answer.status,
14049
+ json: {
14050
+ message: answer.body.trim() === "" ? `The proposals API answered ${answer.status} with no body.` : `The proposals API answered ${answer.status}: ${answer.body.slice(0, 300)}`
14051
+ }
14052
+ };
14053
+ }
14054
+ };
13596
14055
  return {
13597
14056
  "/draft/enable": {
13598
14057
  GET: async req => {
@@ -14026,6 +14485,8 @@ const ValServer = (valModules, options, callbacks) => {
14026
14485
  * shows one story to a project that lives by the other.
14027
14486
  */
14028
14487
  const sourceMode = serverOps.sourceMode();
14488
+ // Spread like the two above: absent is "on the site".
14489
+ const proposal = serverOps.currentProposal();
14029
14490
  return {
14030
14491
  status: 200,
14031
14492
  json: {
@@ -14041,6 +14502,9 @@ const ValServer = (valModules, options, callbacks) => {
14041
14502
  ...(sourceMode ? {
14042
14503
  publishJobs: serverOps.publishesAsJobs()
14043
14504
  } : {}),
14505
+ ...(proposal ? {
14506
+ proposal
14507
+ } : {}),
14044
14508
  // Not `options.config` verbatim: in proxy mode the branch the
14045
14509
  // server resolved is filled in where the file did not name one.
14046
14510
  // See `clientConfig`.
@@ -14090,6 +14554,78 @@ const ValServer = (valModules, options, callbacks) => {
14090
14554
  return proxyPublishApi(req.path, "POST", req.body === undefined ? undefined : JSON.stringify(req.body));
14091
14555
  }
14092
14556
  },
14557
+ /**
14558
+ * The content service's proposals API, reached through this deployment --
14559
+ * the switcher, the list and the dialogs. The session is checked and its
14560
+ * profile sent along, because a proposal is changed by a person; see
14561
+ * `ValOpsHttp.proposalsApi` for what may be reached.
14562
+ */
14563
+ "/proposals-api": {
14564
+ GET: async req => {
14565
+ const auth = getAuth(req.cookies);
14566
+ if (auth.error) {
14567
+ return {
14568
+ status: 401,
14569
+ json: {
14570
+ message: auth.error
14571
+ }
14572
+ };
14573
+ }
14574
+ return proxyProposalsApi(req.path, "GET", profileOf(auth));
14575
+ },
14576
+ POST: async req => {
14577
+ const auth = getAuth(req.cookies);
14578
+ if (auth.error) {
14579
+ return {
14580
+ status: 401,
14581
+ json: {
14582
+ message: auth.error
14583
+ }
14584
+ };
14585
+ }
14586
+ return proxyProposalsApi(req.path, "POST", profileOf(auth), JSON.stringify(req.body ?? {}));
14587
+ },
14588
+ PATCH: async req => {
14589
+ const auth = getAuth(req.cookies);
14590
+ if (auth.error) {
14591
+ return {
14592
+ status: 401,
14593
+ json: {
14594
+ message: auth.error
14595
+ }
14596
+ };
14597
+ }
14598
+ return proxyProposalsApi(req.path, "PATCH", profileOf(auth), JSON.stringify(req.body ?? {}));
14599
+ }
14600
+ },
14601
+ "/proposal-site-sources": {
14602
+ GET: async req => {
14603
+ const auth = getAuth(req.cookies);
14604
+ if (auth.error) {
14605
+ return {
14606
+ status: 401,
14607
+ json: {
14608
+ message: auth.error
14609
+ }
14610
+ };
14611
+ }
14612
+ const modules = await serverOps.siteSourcesUnderSnapshot();
14613
+ if (modules === null) {
14614
+ return {
14615
+ status: 404,
14616
+ json: {
14617
+ message: "This is not a proposal's address."
14618
+ }
14619
+ };
14620
+ }
14621
+ return {
14622
+ status: 200,
14623
+ json: {
14624
+ modules
14625
+ }
14626
+ };
14627
+ }
14628
+ },
14093
14629
  "/upload/patches": {
14094
14630
  POST: async req => {
14095
14631
  if (serverOps instanceof ValOpsHttp) {
@@ -15068,9 +15604,23 @@ const ValServer = (valModules, options, callbacks) => {
15068
15604
  patchIds: undefined,
15069
15605
  excludePatchOps: false
15070
15606
  });
15607
+ /*
15608
+ * Membership from THIS response when it carries one, not from the
15609
+ * group list `resolveOwnPatchScope` read.
15610
+ *
15611
+ * That list is a separate request, remembered for a second
15612
+ * (`ValOpsHttp.getPatchGroups`), so it can be older than the chain
15613
+ * fetched here: an editor who saved and reloaded at once was
15614
+ * rendered without the patch they had just written, because the
15615
+ * list still had their group without it. The content service reads
15616
+ * the chain and each patch's groups in one transaction, and
15617
+ * `fetchPatches` folds that onto `patchGroups` -- so the patches
15618
+ * and who owns them come from the same moment.
15619
+ */
15620
+ const own = ownPatchScopeFor(all, "id" in auth && auth.id || undefined, ownPatchIds);
15071
15621
  patchOps = {
15072
15622
  ...all,
15073
- patches: scopedPatches(all.patches, ownPatchIds)
15623
+ patches: scopedPatches(all.patches, own)
15074
15624
  };
15075
15625
  } else if (requestedPatchIds !== undefined && requestedPatchIds.length === 0) {
15076
15626
  /*
@@ -15695,7 +16245,13 @@ const ValServer = (valModules, options, callbacks) => {
15695
16245
  * tree; a store with no working tree does only the push.
15696
16246
  */
15697
16247
  if (serverOps instanceof ValOpsMemory) {
15698
- const isRemoteRequired = getIsRemoteRequired(await serverOps.getSchemas());
16248
+ /*
16249
+ * A commit with no files uploads nothing and needs no credentials;
16250
+ * see `commitCarriesRemoteFiles`. One with a LOCAL file still goes
16251
+ * through `uploadRemoteFiles` where the project is remote, because
16252
+ * that is where a local file is refused rather than dropped.
16253
+ */
16254
+ const isRemoteRequired = commitCarriesRemoteFiles(preparedCommit) || commitCarriesFiles(preparedCommit) && getIsRemoteRequired(await serverOps.getSchemas());
15699
16255
  if (isRemoteRequired) {
15700
16256
  const authRes = await getRemoteFileAuth();
15701
16257
  if (authRes.status !== 200) {
@@ -15718,7 +16274,9 @@ const ValServer = (valModules, options, callbacks) => {
15718
16274
  }
15719
16275
  if (serverOps instanceof ValOpsFS) {
15720
16276
  var _remoteFileAuthRes;
15721
- const isRemoteRequired = getIsRemoteRequired(await serverOps.getSchemas());
16277
+ // Credentials when THIS save uploads a remote file, not whenever
16278
+ // the project has a remote schema. See `commitCarriesRemoteFiles`.
16279
+ const isRemoteRequired = commitCarriesRemoteFiles(preparedCommit);
15722
16280
  let mode;
15723
16281
  let remoteFileAuthRes;
15724
16282
  if (isRemoteRequired) {
@@ -17035,6 +17593,24 @@ chain, deleted, requested) {
17035
17593
  * `undefined` means "apply everything", which is what every caller that does
17036
17594
  * not ask for scoping gets and must keep getting.
17037
17595
  */
17596
+ /** The patches in this author's OPEN groups: what a scoped draft shows. */
17597
+ function ownPatchIdsIn(patchGroups, authorId) {
17598
+ return patchGroups.filter(group => group.publishedAt === null && group.authorId === authorId).flatMap(group => group.patchIds);
17599
+ }
17600
+
17601
+ /**
17602
+ * Whose pending patches a scoped draft shows, taken from the SAME response as
17603
+ * the patches when it says, and from the list read beforehand only when not.
17604
+ *
17605
+ * See the call in `/sources/~`: the list read beforehand can be older than the
17606
+ * chain, and the response's membership cannot be.
17607
+ */
17608
+ function ownPatchScopeFor(fetched, authorId, remembered) {
17609
+ if (fetched.patchGroups === undefined || authorId === undefined) {
17610
+ return remembered;
17611
+ }
17612
+ return ownPatchIdsIn(fetched.patchGroups, authorId);
17613
+ }
17038
17614
  async function resolveOwnPatchScope(
17039
17615
  /*
17040
17616
  * `ValOps`, not the two concrete stores: the one thing this needs is whether
@@ -17089,7 +17665,7 @@ serverOps, opts) {
17089
17665
  * nothing, and base is the honest answer. Distinct from the
17090
17666
  * case above, which is why the two are not one expression.
17091
17667
  */
17092
- ownPatchIds = groupsRes.patchGroups.filter(group => group.publishedAt === null && group.authorId === opts.authorId).flatMap(group => group.patchIds);
17668
+ ownPatchIds = ownPatchIdsIn(groupsRes.patchGroups, opts.authorId);
17093
17669
  /*
17094
17670
  * Scoping applies to PENDING work only. Anything already
17095
17671
  * committed is part of everyone's view.
@@ -23108,4 +23684,4 @@ function readCapturedReport(snapshotDir) {
23108
23684
  return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
23109
23685
  }
23110
23686
 
23111
- export { APP_MODE_REQUIRES_REMOTE_FILES, DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, EXTERNAL_RESULT, InMemoryPatchStore, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValOpsMemory, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createPrettierFormatter, createService, createValApiRouter, createValModuleFileInspector, createValOps, createValServer, currentFixHandlers, decodeJwtWithoutVerifying, defineExternal, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, err, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, extractVideoMetadata, extractVideoMetadataFromFile, filesOfVideo, filesOfVideosetEntry, findAndEvalValConfigFile, findDefaultExport, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleExternalUpload, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, handleVideosetCheckAllFiles, handleVideosetCheckRemote, handleVideosetMetadata, handleVideosetUploadRemote, initHandlerOptions, isExternalResult, loadValModules, ok, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, planJsonValuesEntryExtraction, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, resolveRemoteFileAuth, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata, verifyJwt, videosetEntryVideoSchema };
23687
+ export { APP_MODE_REQUIRES_REMOTE_FILES, DEFAULT_LOGIN_EXPIRES_IN_SECONDS, DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_POLL_INTERVAL_SECONDS, EXTERNAL_RESULT, InMemoryPatchStore, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValOpsMemory, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, classifyJsonValuesOp, compareWithCapturedReport, createDefaultValFSHost, createFixPatch, createJsonEntryPathMap, createModulePathMap, createPrettierFormatter, createService, createValApiRouter, createValModuleFileInspector, createValOps, createValServer, currentFixHandlers, decodeJwtWithoutVerifying, defineExternal, describePatchStoreProblems, downloadFileFromRemote, encodeJwt, err, evalValConfigFile, extractFileMetadata, extractImageMetadata, extractJsonValuesEntry, extractVideoMetadata, extractVideoMetadataFromFile, filesOfVideo, filesOfVideosetEntry, findAndEvalValConfigFile, findDefaultExport, findJsonEntryFilePath, fixHandlers, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, handleCheckAllFiles, handleExternalUpload, handleFileMetadata, handleJsonValuesExtractEntry, handleRemoteFileCheck, handleRemoteFileDownload, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleUniqueFolderCheck, handleVideosetCheckAllFiles, handleVideosetCheckRemote, handleVideosetMetadata, handleVideosetUploadRemote, initHandlerOptions, isExternalResult, loadValModules, ok, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, planJsonValuesEntryExtraction, proposalFromEnv, proposalSnapshot, readCapturedReport, readPatchStore, rebaseContentOp, replaySnapshot, resolveRemoteFileAuth, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata, verifyJwt, videosetEntryVideoSchema };