@valbuild/server 0.141.1 → 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.
- package/CHANGELOG.md +20 -0
- package/dist/declarations/src/ValOps.d.ts +43 -0
- package/dist/declarations/src/ValOpsHttp.d.ts +62 -0
- package/dist/declarations/src/ValRouter.d.ts +7 -0
- package/dist/declarations/src/ValServer.d.ts +16 -1
- package/dist/declarations/src/index.d.ts +2 -0
- package/dist/declarations/src/proposal.d.ts +72 -0
- package/dist/valbuild-server.cjs.dev.js +587 -35
- package/dist/valbuild-server.cjs.prod.js +587 -35
- package/dist/valbuild-server.esm.js +586 -36
- package/package.json +4 -4
|
@@ -2679,18 +2679,41 @@ function encodeJwt(payload, sessionKey) {
|
|
|
2679
2679
|
* error somebody had to answer instead of a default that silently applied.
|
|
2680
2680
|
*/
|
|
2681
2681
|
function clientConfig(options) {
|
|
2682
|
+
const config = withResolvedProject(options.config, options.project);
|
|
2682
2683
|
if (options.mode !== "http" || options.git === undefined) {
|
|
2683
|
-
return
|
|
2684
|
+
return config;
|
|
2684
2685
|
}
|
|
2685
|
-
if (
|
|
2686
|
-
return
|
|
2686
|
+
if (config.gitBranch !== undefined) {
|
|
2687
|
+
return config;
|
|
2687
2688
|
}
|
|
2688
2689
|
return {
|
|
2689
|
-
...
|
|
2690
|
+
...config,
|
|
2690
2691
|
gitBranch: options.git.branch
|
|
2691
2692
|
};
|
|
2692
2693
|
}
|
|
2693
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
|
+
|
|
2694
2717
|
/**
|
|
2695
2718
|
* Which IP addresses this server may open a connection to.
|
|
2696
2719
|
*
|
|
@@ -3564,6 +3587,12 @@ class ValOps {
|
|
|
3564
3587
|
* hash. Re-folding with the wrong list changes the base SHA for no reason.
|
|
3565
3588
|
*/
|
|
3566
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;
|
|
3567
3596
|
/**
|
|
3568
3597
|
* What a save has told us each `.jsonValues()` entry now holds.
|
|
3569
3598
|
*
|
|
@@ -3610,6 +3639,7 @@ class ValOps {
|
|
|
3610
3639
|
// #region initTree
|
|
3611
3640
|
async initSources() {
|
|
3612
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;
|
|
3613
3643
|
const extracted = await extractValModules(this.valModules);
|
|
3614
3644
|
const moduleErrors = extracted.moduleErrors.concat(findNestedJsonValuesModuleErrors(extracted.schemas));
|
|
3615
3645
|
this.sources = extracted.sources;
|
|
@@ -3621,12 +3651,29 @@ class ValOps {
|
|
|
3621
3651
|
this.modulesErrors = moduleErrors;
|
|
3622
3652
|
this.shaEntries = extracted.shaEntries;
|
|
3623
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
|
+
}
|
|
3624
3671
|
return {
|
|
3625
3672
|
baseSha: this.baseSha,
|
|
3626
3673
|
schemaSha: this.schemaSha,
|
|
3627
3674
|
sourcesSha: this.sourcesSha,
|
|
3628
3675
|
configSha: this.configSha,
|
|
3629
|
-
sources:
|
|
3676
|
+
sources: this.sources,
|
|
3630
3677
|
schemas: extracted.schemas,
|
|
3631
3678
|
moduleErrors
|
|
3632
3679
|
};
|
|
@@ -3642,6 +3689,19 @@ class ValOps {
|
|
|
3642
3689
|
};
|
|
3643
3690
|
}
|
|
3644
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
|
+
|
|
3645
3705
|
/**
|
|
3646
3706
|
* These patches are on disk now: adopt what they produced as the committed
|
|
3647
3707
|
* sources.
|
|
@@ -5238,9 +5298,9 @@ class ValOps {
|
|
|
5238
5298
|
// https://github.com/microsoft/TypeScript/issues/36174
|
|
5239
5299
|
let sourceFileText = null;
|
|
5240
5300
|
if (tsChanged) {
|
|
5241
|
-
var _this$
|
|
5301
|
+
var _this$options3;
|
|
5242
5302
|
sourceFileText = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
|
|
5243
|
-
if ((_this$
|
|
5303
|
+
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.formatter) {
|
|
5244
5304
|
try {
|
|
5245
5305
|
sourceFileText = await this.options.formatter(sourceFileText, path);
|
|
5246
5306
|
} catch (err) {
|
|
@@ -5252,13 +5312,13 @@ class ValOps {
|
|
|
5252
5312
|
}
|
|
5253
5313
|
const extraFiles = {};
|
|
5254
5314
|
for (const [jsonPath, content] of Array.from(jsonEntryContents)) {
|
|
5255
|
-
var _this$
|
|
5315
|
+
var _this$options4;
|
|
5256
5316
|
if (content === null) {
|
|
5257
5317
|
extraFiles[jsonPath] = null;
|
|
5258
5318
|
continue;
|
|
5259
5319
|
}
|
|
5260
5320
|
let jsonText = JSON.stringify(content, null, 2);
|
|
5261
|
-
if ((_this$
|
|
5321
|
+
if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.formatter) {
|
|
5262
5322
|
try {
|
|
5263
5323
|
jsonText = await this.options.formatter(jsonText, jsonPath);
|
|
5264
5324
|
} catch (err) {
|
|
@@ -5574,6 +5634,30 @@ class ValOps {
|
|
|
5574
5634
|
};
|
|
5575
5635
|
}
|
|
5576
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
|
+
|
|
5577
5661
|
/**
|
|
5578
5662
|
* Whether a commit here produces `.val.ts` TEXT as well as data.
|
|
5579
5663
|
*
|
|
@@ -8777,6 +8861,111 @@ function computePatchesToDrop(prepared) {
|
|
|
8777
8861
|
return dropped;
|
|
8778
8862
|
}
|
|
8779
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
|
+
|
|
8780
8969
|
const textEncoder$2 = new TextEncoder();
|
|
8781
8970
|
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
|
8782
8971
|
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
|
@@ -9130,6 +9319,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9130
9319
|
publishJob;
|
|
9131
9320
|
/** See `publishBuild` on {@link ValApiOptions}; sent with every position. */
|
|
9132
9321
|
publishBuild;
|
|
9322
|
+
/** See `proposal` on {@link ValApiOptions}; `null` on the site. */
|
|
9323
|
+
proposal;
|
|
9133
9324
|
|
|
9134
9325
|
/** Did the host hand over the running build's source? See `projectSource`. */
|
|
9135
9326
|
embedsSource() {
|
|
@@ -9202,7 +9393,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9202
9393
|
* getSettings / uploadRemoteFile / getPresignedAuthNonce.
|
|
9203
9394
|
*/
|
|
9204
9395
|
auth, valModules, options) {
|
|
9205
|
-
super(valModules, options
|
|
9396
|
+
super(valModules, options !== null && options !== void 0 && options.proposal ? {
|
|
9397
|
+
...options,
|
|
9398
|
+
snapshotSources: proposalSnapshot(options.proposal)
|
|
9399
|
+
} : options);
|
|
9206
9400
|
this.contentUrl = contentUrl;
|
|
9207
9401
|
this.project = project;
|
|
9208
9402
|
this.git = git;
|
|
@@ -9212,7 +9406,15 @@ class ValOpsHttp extends ValOps {
|
|
|
9212
9406
|
Authorization: `Bearer ${auth.apiKey}`
|
|
9213
9407
|
};
|
|
9214
9408
|
this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
|
|
9215
|
-
this.
|
|
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;
|
|
9216
9418
|
this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
|
|
9217
9419
|
this.publishBuild = (options === null || options === void 0 ? void 0 : options.publishBuild) ?? null;
|
|
9218
9420
|
this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
|
|
@@ -9241,7 +9443,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9241
9443
|
*/
|
|
9242
9444
|
publishRefusal() {
|
|
9243
9445
|
var _this$projectExpectat;
|
|
9244
|
-
|
|
9446
|
+
// A save in a proposal mirrors nothing: it reaches git by merging.
|
|
9447
|
+
if (this.git !== null || this.proposal !== null) {
|
|
9245
9448
|
return null;
|
|
9246
9449
|
}
|
|
9247
9450
|
/*
|
|
@@ -9278,6 +9481,13 @@ class ValOpsHttp extends ValOps {
|
|
|
9278
9481
|
*/
|
|
9279
9482
|
sourceMode() {
|
|
9280
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;
|
|
9281
9491
|
return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
|
|
9282
9492
|
}
|
|
9283
9493
|
|
|
@@ -9312,6 +9522,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9312
9522
|
}).catch(() => undefined);
|
|
9313
9523
|
}
|
|
9314
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;
|
|
9315
9527
|
const expected = this.projectExpectation;
|
|
9316
9528
|
if (expected === null) return false;
|
|
9317
9529
|
return expected.publishJobs ?? expected.sourceMode === "managed";
|
|
@@ -9431,6 +9643,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9431
9643
|
* segment gets through.
|
|
9432
9644
|
*/
|
|
9433
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
|
+
}
|
|
9434
9650
|
if (path === "/build-target" || path === "/project-source" || path === "/update-target" || path === "/publish" ||
|
|
9435
9651
|
// Where "View run" goes, for a connected build CI reported failed.
|
|
9436
9652
|
// Read only: reporting a run is CI's, with its own token.
|
|
@@ -9453,6 +9669,20 @@ class ValOpsHttp extends ValOps {
|
|
|
9453
9669
|
return /^\/publish\/[A-Za-z0-9_-]+(\/(artifacts|verify|promote))?$/.test(path);
|
|
9454
9670
|
}
|
|
9455
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
|
+
}
|
|
9456
9686
|
if (!ValOpsHttp.publishApiPathAllowed(path)) {
|
|
9457
9687
|
return {
|
|
9458
9688
|
status: 403,
|
|
@@ -9464,6 +9694,82 @@ class ValOpsHttp extends ValOps {
|
|
|
9464
9694
|
}
|
|
9465
9695
|
return this.callPublishApi(path, init);
|
|
9466
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
|
+
}
|
|
9467
9773
|
|
|
9468
9774
|
/**
|
|
9469
9775
|
* A publish job's prepare, sent to content: the job's sources, archived
|
|
@@ -9764,6 +10070,42 @@ class ValOpsHttp extends ValOps {
|
|
|
9764
10070
|
} : {})
|
|
9765
10071
|
};
|
|
9766
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
|
+
}
|
|
9767
10109
|
async getWebSocketNonce(profileId) {
|
|
9768
10110
|
return fetch(`${this.contentUrl}/v1/${this.project}/websocket/nonces`, {
|
|
9769
10111
|
method: "POST",
|
|
@@ -9775,12 +10117,24 @@ class ValOpsHttp extends ValOps {
|
|
|
9775
10117
|
* the project -- and a nonce is scoped to the project and the person,
|
|
9776
10118
|
* not to a position in a chain.
|
|
9777
10119
|
*/
|
|
9778
|
-
...(
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
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
|
+
})(),
|
|
9784
10138
|
// Which build is asking, beside what it says about itself.
|
|
9785
10139
|
...(this.publishBuild !== null ? {
|
|
9786
10140
|
build: this.publishBuild
|
|
@@ -9962,12 +10316,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9962
10316
|
* holds the chain, and for a project it is the only publisher of, the
|
|
9963
10317
|
* position IS the head.
|
|
9964
10318
|
*/
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
// A build a tab made has no commit, and says which job it was made for.
|
|
9970
|
-
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]);
|
|
9971
10323
|
}
|
|
9972
10324
|
/*
|
|
9973
10325
|
* And WHICH build this is, when the platform running it says: the only
|
|
@@ -10240,6 +10592,23 @@ class ValOpsHttp extends ValOps {
|
|
|
10240
10592
|
* cache: a second render a second later asks again.
|
|
10241
10593
|
*/
|
|
10242
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
|
+
}
|
|
10243
10612
|
async getPatchGroups(options) {
|
|
10244
10613
|
const now = Date.now();
|
|
10245
10614
|
if ((options === null || options === void 0 ? void 0 : options.fresh) !== true && this.patchGroupsCache !== null && now - this.patchGroupsCache.at < PATCH_GROUPS_CACHE_MS) {
|
|
@@ -10271,6 +10640,17 @@ class ValOpsHttp extends ValOps {
|
|
|
10271
10640
|
return res;
|
|
10272
10641
|
}
|
|
10273
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
|
+
}
|
|
10274
10654
|
try {
|
|
10275
10655
|
/*
|
|
10276
10656
|
* `branch` is REQUIRED by the endpoint, which answers 400 without it.
|
|
@@ -10279,7 +10659,8 @@ class ValOpsHttp extends ValOps {
|
|
|
10279
10659
|
* `saveSourceFilePatch`): groups are per branch, so a request without one
|
|
10280
10660
|
* is not merely under-specified, it is rejected.
|
|
10281
10661
|
*/
|
|
10282
|
-
const
|
|
10662
|
+
const branch = this.ownBranch();
|
|
10663
|
+
const params = new URLSearchParams(branch !== undefined ? [["branch", branch]] : []);
|
|
10283
10664
|
const res = await fetch(`${this.contentUrl}/v1/${this.project}/patch-groups?${params}`, {
|
|
10284
10665
|
headers: this.authHeaders
|
|
10285
10666
|
});
|
|
@@ -10330,8 +10711,9 @@ class ValOpsHttp extends ValOps {
|
|
|
10330
10711
|
* Without one the content API uses the project's own, as it does for reads.
|
|
10331
10712
|
*/
|
|
10332
10713
|
ownGroupBranch() {
|
|
10333
|
-
|
|
10334
|
-
|
|
10714
|
+
const branch = this.ownBranch();
|
|
10715
|
+
return branch !== undefined ? {
|
|
10716
|
+
branch
|
|
10335
10717
|
} : {};
|
|
10336
10718
|
}
|
|
10337
10719
|
async mutatePatchGroup(method, path, body,
|
|
@@ -10354,6 +10736,22 @@ class ValOpsHttp extends ValOps {
|
|
|
10354
10736
|
* on its own there and this adds nothing.
|
|
10355
10737
|
*/
|
|
10356
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) {
|
|
10357
10755
|
try {
|
|
10358
10756
|
const res = await fetch(`${this.contentUrl}/v1/${this.project}/${path}`, {
|
|
10359
10757
|
method,
|
|
@@ -10446,7 +10844,13 @@ class ValOpsHttp extends ValOps {
|
|
|
10446
10844
|
patchId,
|
|
10447
10845
|
parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
|
|
10448
10846
|
baseSha,
|
|
10449
|
-
|
|
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 ? {
|
|
10450
10854
|
commit: this.git.commit,
|
|
10451
10855
|
branch: this.git.branch
|
|
10452
10856
|
} : {}),
|
|
@@ -10523,7 +10927,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10523
10927
|
errorType: "other",
|
|
10524
10928
|
message: `Could save source file patch (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
|
10525
10929
|
});
|
|
10526
|
-
});
|
|
10930
|
+
}).finally(() => this.forgetPatchGroups());
|
|
10527
10931
|
}
|
|
10528
10932
|
|
|
10529
10933
|
/**
|
|
@@ -10815,12 +11219,11 @@ class ValOpsHttp extends ValOps {
|
|
|
10815
11219
|
if (res.ok) {
|
|
10816
11220
|
const parsed = DeletePatchesResponse.safeParse(await res.json());
|
|
10817
11221
|
if (parsed.success) {
|
|
10818
|
-
var _this$git;
|
|
10819
11222
|
const errors = {};
|
|
10820
11223
|
for (const err of parsed.data.errors || []) {
|
|
10821
11224
|
errors[err.patchId] = err;
|
|
10822
11225
|
}
|
|
10823
|
-
const headVersion = ownBranchVersion(parsed.data.headVersions,
|
|
11226
|
+
const headVersion = ownBranchVersion(parsed.data.headVersions, this.ownBranch());
|
|
10824
11227
|
const version = headVersion !== undefined ? {
|
|
10825
11228
|
headVersion
|
|
10826
11229
|
} : {};
|
|
@@ -10853,7 +11256,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10853
11256
|
message: `Could not delete patches (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
|
10854
11257
|
}
|
|
10855
11258
|
};
|
|
10856
|
-
});
|
|
11259
|
+
}).finally(() => this.forgetPatchGroups());
|
|
10857
11260
|
}
|
|
10858
11261
|
async commit(prepared, message, committer, filesDirectory, newBranch,
|
|
10859
11262
|
/**
|
|
@@ -10873,7 +11276,13 @@ class ValOpsHttp extends ValOps {
|
|
|
10873
11276
|
patchGroupId) {
|
|
10874
11277
|
try {
|
|
10875
11278
|
var _res$headers$get3;
|
|
10876
|
-
|
|
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, {
|
|
10877
11286
|
method: "POST",
|
|
10878
11287
|
headers: {
|
|
10879
11288
|
...this.authHeaders,
|
|
@@ -12938,6 +13347,9 @@ async function initHandlerOptions(route, opts, config) {
|
|
|
12938
13347
|
...(opts.publishBuild !== undefined ? {
|
|
12939
13348
|
publishBuild: opts.publishBuild
|
|
12940
13349
|
} : {}),
|
|
13350
|
+
...(opts.proposal !== undefined ? {
|
|
13351
|
+
proposal: opts.proposal
|
|
13352
|
+
} : {}),
|
|
12941
13353
|
project: maybeValProject,
|
|
12942
13354
|
valEnableRedirectUrl,
|
|
12943
13355
|
valDisableRedirectUrl,
|
|
@@ -13013,6 +13425,9 @@ function createValOps(valModules, options) {
|
|
|
13013
13425
|
} : {}),
|
|
13014
13426
|
...(options.publishBuild !== undefined ? {
|
|
13015
13427
|
publishBuild: options.publishBuild
|
|
13428
|
+
} : {}),
|
|
13429
|
+
...(options.proposal !== undefined ? {
|
|
13430
|
+
proposal: options.proposal
|
|
13016
13431
|
} : {})
|
|
13017
13432
|
});
|
|
13018
13433
|
}
|
|
@@ -13611,6 +14026,32 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
13611
14026
|
};
|
|
13612
14027
|
}
|
|
13613
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
|
+
};
|
|
13614
14055
|
return {
|
|
13615
14056
|
"/draft/enable": {
|
|
13616
14057
|
GET: async req => {
|
|
@@ -14044,6 +14485,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14044
14485
|
* shows one story to a project that lives by the other.
|
|
14045
14486
|
*/
|
|
14046
14487
|
const sourceMode = serverOps.sourceMode();
|
|
14488
|
+
// Spread like the two above: absent is "on the site".
|
|
14489
|
+
const proposal = serverOps.currentProposal();
|
|
14047
14490
|
return {
|
|
14048
14491
|
status: 200,
|
|
14049
14492
|
json: {
|
|
@@ -14059,6 +14502,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14059
14502
|
...(sourceMode ? {
|
|
14060
14503
|
publishJobs: serverOps.publishesAsJobs()
|
|
14061
14504
|
} : {}),
|
|
14505
|
+
...(proposal ? {
|
|
14506
|
+
proposal
|
|
14507
|
+
} : {}),
|
|
14062
14508
|
// Not `options.config` verbatim: in proxy mode the branch the
|
|
14063
14509
|
// server resolved is filled in where the file did not name one.
|
|
14064
14510
|
// See `clientConfig`.
|
|
@@ -14108,6 +14554,78 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14108
14554
|
return proxyPublishApi(req.path, "POST", req.body === undefined ? undefined : JSON.stringify(req.body));
|
|
14109
14555
|
}
|
|
14110
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
|
+
},
|
|
14111
14629
|
"/upload/patches": {
|
|
14112
14630
|
POST: async req => {
|
|
14113
14631
|
if (serverOps instanceof ValOpsHttp) {
|
|
@@ -15086,9 +15604,23 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15086
15604
|
patchIds: undefined,
|
|
15087
15605
|
excludePatchOps: false
|
|
15088
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);
|
|
15089
15621
|
patchOps = {
|
|
15090
15622
|
...all,
|
|
15091
|
-
patches: scopedPatches(all.patches,
|
|
15623
|
+
patches: scopedPatches(all.patches, own)
|
|
15092
15624
|
};
|
|
15093
15625
|
} else if (requestedPatchIds !== undefined && requestedPatchIds.length === 0) {
|
|
15094
15626
|
/*
|
|
@@ -17061,6 +17593,24 @@ chain, deleted, requested) {
|
|
|
17061
17593
|
* `undefined` means "apply everything", which is what every caller that does
|
|
17062
17594
|
* not ask for scoping gets and must keep getting.
|
|
17063
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
|
+
}
|
|
17064
17614
|
async function resolveOwnPatchScope(
|
|
17065
17615
|
/*
|
|
17066
17616
|
* `ValOps`, not the two concrete stores: the one thing this needs is whether
|
|
@@ -17115,7 +17665,7 @@ serverOps, opts) {
|
|
|
17115
17665
|
* nothing, and base is the honest answer. Distinct from the
|
|
17116
17666
|
* case above, which is why the two are not one expression.
|
|
17117
17667
|
*/
|
|
17118
|
-
ownPatchIds = groupsRes.patchGroups
|
|
17668
|
+
ownPatchIds = ownPatchIdsIn(groupsRes.patchGroups, opts.authorId);
|
|
17119
17669
|
/*
|
|
17120
17670
|
* Scoping applies to PENDING work only. Anything already
|
|
17121
17671
|
* committed is part of everyone's view.
|
|
@@ -23134,4 +23684,4 @@ function readCapturedReport(snapshotDir) {
|
|
|
23134
23684
|
return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
23135
23685
|
}
|
|
23136
23686
|
|
|
23137
|
-
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 };
|