@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
|
@@ -2716,18 +2716,41 @@ function encodeJwt(payload, sessionKey) {
|
|
|
2716
2716
|
* error somebody had to answer instead of a default that silently applied.
|
|
2717
2717
|
*/
|
|
2718
2718
|
function clientConfig(options) {
|
|
2719
|
+
const config = withResolvedProject(options.config, options.project);
|
|
2719
2720
|
if (options.mode !== "http" || options.git === undefined) {
|
|
2720
|
-
return
|
|
2721
|
+
return config;
|
|
2721
2722
|
}
|
|
2722
|
-
if (
|
|
2723
|
-
return
|
|
2723
|
+
if (config.gitBranch !== undefined) {
|
|
2724
|
+
return config;
|
|
2724
2725
|
}
|
|
2725
2726
|
return {
|
|
2726
|
-
...
|
|
2727
|
+
...config,
|
|
2727
2728
|
gitBranch: options.git.branch
|
|
2728
2729
|
};
|
|
2729
2730
|
}
|
|
2730
2731
|
|
|
2732
|
+
/**
|
|
2733
|
+
* The project, filled in the same way and for the same reason as the branch.
|
|
2734
|
+
*
|
|
2735
|
+
* `project` may come from `VAL_PROJECT` rather than from `val.config`, and the
|
|
2736
|
+
* server talks to that project either way. The Studio decides from
|
|
2737
|
+
* `config.project` whether there is an assistant to offer at all (`wsEnabled`
|
|
2738
|
+
* in `ValProvider`), so a project named only in the environment would have
|
|
2739
|
+
* had it hidden while the server behind it was ready to serve it.
|
|
2740
|
+
*
|
|
2741
|
+
* `val.config` wins, as it does for the branch, and every mode gets it: unlike
|
|
2742
|
+
* a branch, a project means the same thing in fs mode as in http mode.
|
|
2743
|
+
*/
|
|
2744
|
+
function withResolvedProject(config, project) {
|
|
2745
|
+
if (config.project !== undefined || project === undefined) {
|
|
2746
|
+
return config;
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
...config,
|
|
2750
|
+
project
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2731
2754
|
/**
|
|
2732
2755
|
* Which IP addresses this server may open a connection to.
|
|
2733
2756
|
*
|
|
@@ -3601,6 +3624,12 @@ class ValOps {
|
|
|
3601
3624
|
* hash. Re-folding with the wrong list changes the base SHA for no reason.
|
|
3602
3625
|
*/
|
|
3603
3626
|
shaModuleErrors;
|
|
3627
|
+
/**
|
|
3628
|
+
* The SITE's Source for the modules a proposal's snapshot replaced: what the
|
|
3629
|
+
* bundle evaluated to before the snapshot went over it. Kept for
|
|
3630
|
+
* {@link siteSourcesUnderSnapshot}, and only where there is a snapshot.
|
|
3631
|
+
*/
|
|
3632
|
+
siteSourcesBeforeSnapshot = null;
|
|
3604
3633
|
/**
|
|
3605
3634
|
* What a save has told us each `.jsonValues()` entry now holds.
|
|
3606
3635
|
*
|
|
@@ -3647,6 +3676,7 @@ class ValOps {
|
|
|
3647
3676
|
// #region initTree
|
|
3648
3677
|
async initSources() {
|
|
3649
3678
|
if (this.baseSha === null || this.sourcesSha === null || this.configSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
|
|
3679
|
+
var _this$options;
|
|
3650
3680
|
const extracted = await core.extractValModules(this.valModules);
|
|
3651
3681
|
const moduleErrors = extracted.moduleErrors.concat(findNestedJsonValuesModuleErrors(extracted.schemas));
|
|
3652
3682
|
this.sources = extracted.sources;
|
|
@@ -3658,12 +3688,29 @@ class ValOps {
|
|
|
3658
3688
|
this.modulesErrors = moduleErrors;
|
|
3659
3689
|
this.shaEntries = extracted.shaEntries;
|
|
3660
3690
|
this.shaModuleErrors = extracted.moduleErrors;
|
|
3691
|
+
/*
|
|
3692
|
+
* The snapshot, over what the modules evaluated to. Through the same
|
|
3693
|
+
* door a save uses, so the SHAs move with it: they describe the Source
|
|
3694
|
+
* being served, and a base SHA that still described the bundle would
|
|
3695
|
+
* say the snapshot's content was the bundle's.
|
|
3696
|
+
*/
|
|
3697
|
+
if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.snapshotSources) {
|
|
3698
|
+
const site = {};
|
|
3699
|
+
for (const moduleFilePath of Object.keys(this.options.snapshotSources)) {
|
|
3700
|
+
const source = extracted.sources[moduleFilePath];
|
|
3701
|
+
if (source !== undefined) {
|
|
3702
|
+
site[moduleFilePath] = source;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
this.siteSourcesBeforeSnapshot = site;
|
|
3706
|
+
this.promoteCommittedSources(this.options.snapshotSources);
|
|
3707
|
+
}
|
|
3661
3708
|
return {
|
|
3662
3709
|
baseSha: this.baseSha,
|
|
3663
3710
|
schemaSha: this.schemaSha,
|
|
3664
3711
|
sourcesSha: this.sourcesSha,
|
|
3665
3712
|
configSha: this.configSha,
|
|
3666
|
-
sources:
|
|
3713
|
+
sources: this.sources,
|
|
3667
3714
|
schemas: extracted.schemas,
|
|
3668
3715
|
moduleErrors
|
|
3669
3716
|
};
|
|
@@ -3679,6 +3726,19 @@ class ValOps {
|
|
|
3679
3726
|
};
|
|
3680
3727
|
}
|
|
3681
3728
|
|
|
3729
|
+
/**
|
|
3730
|
+
* In a proposal: the SITE's Source for every module the proposal has saved,
|
|
3731
|
+
* as the build it is based on evaluated it -- what the proposal is compared
|
|
3732
|
+
* against, and what merging it changes. Every other module is the same in
|
|
3733
|
+
* both, so it is not sent. `null` anywhere that is not a proposal.
|
|
3734
|
+
*/
|
|
3735
|
+
async siteSourcesUnderSnapshot() {
|
|
3736
|
+
var _this$options2;
|
|
3737
|
+
if (!((_this$options2 = this.options) !== null && _this$options2 !== void 0 && _this$options2.snapshotSources)) return null;
|
|
3738
|
+
await this.initSources();
|
|
3739
|
+
return this.siteSourcesBeforeSnapshot;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3682
3742
|
/**
|
|
3683
3743
|
* These patches are on disk now: adopt what they produced as the committed
|
|
3684
3744
|
* sources.
|
|
@@ -5275,9 +5335,9 @@ class ValOps {
|
|
|
5275
5335
|
// https://github.com/microsoft/TypeScript/issues/36174
|
|
5276
5336
|
let sourceFileText = null;
|
|
5277
5337
|
if (tsChanged) {
|
|
5278
|
-
var _this$
|
|
5338
|
+
var _this$options3;
|
|
5279
5339
|
sourceFileText = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
|
|
5280
|
-
if ((_this$
|
|
5340
|
+
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.formatter) {
|
|
5281
5341
|
try {
|
|
5282
5342
|
sourceFileText = await this.options.formatter(sourceFileText, path);
|
|
5283
5343
|
} catch (err) {
|
|
@@ -5289,13 +5349,13 @@ class ValOps {
|
|
|
5289
5349
|
}
|
|
5290
5350
|
const extraFiles = {};
|
|
5291
5351
|
for (const [jsonPath, content] of Array.from(jsonEntryContents)) {
|
|
5292
|
-
var _this$
|
|
5352
|
+
var _this$options4;
|
|
5293
5353
|
if (content === null) {
|
|
5294
5354
|
extraFiles[jsonPath] = null;
|
|
5295
5355
|
continue;
|
|
5296
5356
|
}
|
|
5297
5357
|
let jsonText = JSON.stringify(content, null, 2);
|
|
5298
|
-
if ((_this$
|
|
5358
|
+
if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.formatter) {
|
|
5299
5359
|
try {
|
|
5300
5360
|
jsonText = await this.options.formatter(jsonText, jsonPath);
|
|
5301
5361
|
} catch (err) {
|
|
@@ -5611,6 +5671,30 @@ class ValOps {
|
|
|
5611
5671
|
};
|
|
5612
5672
|
}
|
|
5613
5673
|
|
|
5674
|
+
/**
|
|
5675
|
+
* The proposal this server runs at the address of, or `null` on the site --
|
|
5676
|
+
* and always in `fs` and memory mode, which have no proposals. See
|
|
5677
|
+
* `ValProposal`.
|
|
5678
|
+
*/
|
|
5679
|
+
currentProposal() {
|
|
5680
|
+
return null;
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5683
|
+
/**
|
|
5684
|
+
* The content service's proposals API, for the Studio's switcher, list and
|
|
5685
|
+
* dialogs. Refused here for the reason {@link publishApi} is: there is no
|
|
5686
|
+
* content service to forward to. `profileId` is the person asking.
|
|
5687
|
+
*/
|
|
5688
|
+
async proposalsApi(_path, _init, _profileId) {
|
|
5689
|
+
return {
|
|
5690
|
+
status: 501,
|
|
5691
|
+
contentType: "application/json",
|
|
5692
|
+
body: JSON.stringify({
|
|
5693
|
+
message: "This Val server has no content service, so it has no proposals."
|
|
5694
|
+
})
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5614
5698
|
/**
|
|
5615
5699
|
* Whether a commit here produces `.val.ts` TEXT as well as data.
|
|
5616
5700
|
*
|
|
@@ -8814,6 +8898,111 @@ function computePatchesToDrop(prepared) {
|
|
|
8814
8898
|
return dropped;
|
|
8815
8899
|
}
|
|
8816
8900
|
|
|
8901
|
+
/**
|
|
8902
|
+
* A proposal this server runs at the address of: an editor-managed branch of
|
|
8903
|
+
* the project's content, served from its own copy of the content the
|
|
8904
|
+
* proposal has saved -- the **snapshot**. valbuild/home `docs/proposals.md`,
|
|
8905
|
+
* "Saving".
|
|
8906
|
+
*
|
|
8907
|
+
* A third place Source can come from, beside the two Val already has:
|
|
8908
|
+
*
|
|
8909
|
+
* | source provider | Source comes from |
|
|
8910
|
+
* | --------------- | -------------------------------------- |
|
|
8911
|
+
* | committed | the bundle |
|
|
8912
|
+
* | draft | content, pending patches applied |
|
|
8913
|
+
* | snapshot | the proposal's saves |
|
|
8914
|
+
*
|
|
8915
|
+
* The snapshot REPLACES the bundle's Source for the modules the proposal has
|
|
8916
|
+
* saved and nothing else, on every read: a committed render, the draft a page
|
|
8917
|
+
* starts from, the Studio's tree, validation and the next save's prepare. The
|
|
8918
|
+
* proposal's pending patches apply on top of it, as the site's apply on top of
|
|
8919
|
+
* the bundle.
|
|
8920
|
+
*
|
|
8921
|
+
* Handed over by the platform that serves the address, per isolate: the
|
|
8922
|
+
* snapshot only changes with a save, and a save is a new isolate.
|
|
8923
|
+
*/
|
|
8924
|
+
|
|
8925
|
+
/**
|
|
8926
|
+
* The proposal a platform describes in its runtime env, or `undefined` when
|
|
8927
|
+
* this is not a proposal's address.
|
|
8928
|
+
*
|
|
8929
|
+
* `VAL_PROPOSAL` and `VAL_BRANCH` name it; `VAL_OVERLAY` is the snapshot as
|
|
8930
|
+
* JSON (`{ commit, modules, files }`). Throws on a snapshot that does not
|
|
8931
|
+
* parse rather than serving the base build as though it were the proposal: a
|
|
8932
|
+
* reviewer would be looking at the wrong content and nothing would say so.
|
|
8933
|
+
*/
|
|
8934
|
+
function proposalFromEnv(env) {
|
|
8935
|
+
const name = env.VAL_PROPOSAL;
|
|
8936
|
+
const branch = env.VAL_BRANCH;
|
|
8937
|
+
if (!name || !branch) {
|
|
8938
|
+
return undefined;
|
|
8939
|
+
}
|
|
8940
|
+
let parsed = {};
|
|
8941
|
+
if (env.VAL_OVERLAY) {
|
|
8942
|
+
try {
|
|
8943
|
+
parsed = JSON.parse(env.VAL_OVERLAY);
|
|
8944
|
+
} catch (err) {
|
|
8945
|
+
throw new Error(`Val: proposal ${name}: VAL_OVERLAY is not JSON (${err instanceof Error ? err.message : String(err)})`, {
|
|
8946
|
+
cause: err
|
|
8947
|
+
});
|
|
8948
|
+
}
|
|
8949
|
+
}
|
|
8950
|
+
if (!isObject$1(parsed)) {
|
|
8951
|
+
throw new Error(`Val: proposal ${name}: VAL_OVERLAY is not an object`);
|
|
8952
|
+
}
|
|
8953
|
+
const modulesIn = parsed["modules"] ?? {};
|
|
8954
|
+
const filesIn = parsed["files"] ?? {};
|
|
8955
|
+
const commit = parsed["commit"] ?? null;
|
|
8956
|
+
if (!isObject$1(modulesIn) || !isObject$1(filesIn)) {
|
|
8957
|
+
throw new Error(`Val: proposal ${name}: VAL_OVERLAY's modules and files must be objects`);
|
|
8958
|
+
}
|
|
8959
|
+
if (commit !== null && typeof commit !== "string") {
|
|
8960
|
+
throw new Error(`Val: proposal ${name}: VAL_OVERLAY's commit must be a string or null`);
|
|
8961
|
+
}
|
|
8962
|
+
const files = {};
|
|
8963
|
+
for (const [path, text] of Object.entries(filesIn)) {
|
|
8964
|
+
if (typeof text === "string") {
|
|
8965
|
+
files[path] = text;
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
return {
|
|
8969
|
+
name,
|
|
8970
|
+
branch,
|
|
8971
|
+
commit,
|
|
8972
|
+
modules: modulesIn,
|
|
8973
|
+
files
|
|
8974
|
+
};
|
|
8975
|
+
}
|
|
8976
|
+
|
|
8977
|
+
/**
|
|
8978
|
+
* The proposal's saved Source, as Source: every module whose value is JSON.
|
|
8979
|
+
* One place, so the server and the readers that render without it agree on
|
|
8980
|
+
* what the snapshot holds.
|
|
8981
|
+
*/
|
|
8982
|
+
function proposalSnapshot(proposal) {
|
|
8983
|
+
const snapshot = {};
|
|
8984
|
+
for (const [path, source] of Object.entries(proposal.modules)) {
|
|
8985
|
+
if (isJsonValue(source)) {
|
|
8986
|
+
snapshot[path] = source;
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
return snapshot;
|
|
8990
|
+
}
|
|
8991
|
+
function isObject$1(value) {
|
|
8992
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8993
|
+
}
|
|
8994
|
+
|
|
8995
|
+
/** What `JSON.parse` can produce, checked rather than assumed. */
|
|
8996
|
+
function isJsonValue(value) {
|
|
8997
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
8998
|
+
return true;
|
|
8999
|
+
}
|
|
9000
|
+
if (Array.isArray(value)) {
|
|
9001
|
+
return value.every(isJsonValue);
|
|
9002
|
+
}
|
|
9003
|
+
return isObject$1(value) && Object.values(value).every(isJsonValue);
|
|
9004
|
+
}
|
|
9005
|
+
|
|
8817
9006
|
const textEncoder$2 = new TextEncoder();
|
|
8818
9007
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
8819
9008
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -9167,6 +9356,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9167
9356
|
publishJob;
|
|
9168
9357
|
/** See `publishBuild` on {@link ValApiOptions}; sent with every position. */
|
|
9169
9358
|
publishBuild;
|
|
9359
|
+
/** See `proposal` on {@link ValApiOptions}; `null` on the site. */
|
|
9360
|
+
proposal;
|
|
9170
9361
|
|
|
9171
9362
|
/** Did the host hand over the running build's source? See `projectSource`. */
|
|
9172
9363
|
embedsSource() {
|
|
@@ -9239,7 +9430,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9239
9430
|
* getSettings / uploadRemoteFile / getPresignedAuthNonce.
|
|
9240
9431
|
*/
|
|
9241
9432
|
auth, valModules, options) {
|
|
9242
|
-
super(valModules, options
|
|
9433
|
+
super(valModules, options !== null && options !== void 0 && options.proposal ? {
|
|
9434
|
+
...options,
|
|
9435
|
+
snapshotSources: proposalSnapshot(options.proposal)
|
|
9436
|
+
} : options);
|
|
9243
9437
|
this.contentUrl = contentUrl;
|
|
9244
9438
|
this.project = project;
|
|
9245
9439
|
this.git = git;
|
|
@@ -9249,7 +9443,15 @@ class ValOpsHttp extends ValOps {
|
|
|
9249
9443
|
Authorization: `Bearer ${auth.apiKey}`
|
|
9250
9444
|
};
|
|
9251
9445
|
this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
|
|
9252
|
-
this.
|
|
9446
|
+
this.proposal = (options === null || options === void 0 ? void 0 : options.proposal) ?? null;
|
|
9447
|
+
/*
|
|
9448
|
+
* In a proposal, the text its saves wrote over the text the build was made
|
|
9449
|
+
* from: the next save patches the last one's `.val.ts`, not the base's.
|
|
9450
|
+
*/
|
|
9451
|
+
this.projectSource = this.proposal !== null ? {
|
|
9452
|
+
...((options === null || options === void 0 ? void 0 : options.projectSource) ?? {}),
|
|
9453
|
+
...this.proposal.files
|
|
9454
|
+
} : (options === null || options === void 0 ? void 0 : options.projectSource) ?? null;
|
|
9253
9455
|
this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
|
|
9254
9456
|
this.publishBuild = (options === null || options === void 0 ? void 0 : options.publishBuild) ?? null;
|
|
9255
9457
|
this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
|
|
@@ -9278,7 +9480,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9278
9480
|
*/
|
|
9279
9481
|
publishRefusal() {
|
|
9280
9482
|
var _this$projectExpectat;
|
|
9281
|
-
|
|
9483
|
+
// A save in a proposal mirrors nothing: it reaches git by merging.
|
|
9484
|
+
if (this.git !== null || this.proposal !== null) {
|
|
9282
9485
|
return null;
|
|
9283
9486
|
}
|
|
9284
9487
|
/*
|
|
@@ -9315,6 +9518,13 @@ class ValOpsHttp extends ValOps {
|
|
|
9315
9518
|
*/
|
|
9316
9519
|
sourceMode() {
|
|
9317
9520
|
var _this$projectExpectat2;
|
|
9521
|
+
/*
|
|
9522
|
+
* Not reported in a proposal, whatever the project is. The Studio reads
|
|
9523
|
+
* `managed` as "I am the deployer" and follows a commit with a build it
|
|
9524
|
+
* publishes to the SITE -- and a proposal's commit is a Save, which its
|
|
9525
|
+
* address serves without any build at all.
|
|
9526
|
+
*/
|
|
9527
|
+
if (this.proposal !== null) return null;
|
|
9318
9528
|
return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
|
|
9319
9529
|
}
|
|
9320
9530
|
|
|
@@ -9349,6 +9559,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9349
9559
|
}).catch(() => undefined);
|
|
9350
9560
|
}
|
|
9351
9561
|
publishesAsJobs() {
|
|
9562
|
+
// A proposal's Save is a commit on its branch; its merge is the publish.
|
|
9563
|
+
if (this.proposal !== null) return false;
|
|
9352
9564
|
const expected = this.projectExpectation;
|
|
9353
9565
|
if (expected === null) return false;
|
|
9354
9566
|
return expected.publishJobs ?? expected.sourceMode === "managed";
|
|
@@ -9468,6 +9680,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9468
9680
|
* segment gets through.
|
|
9469
9681
|
*/
|
|
9470
9682
|
static publishApiPathAllowed(path) {
|
|
9683
|
+
// A merge's prepare is content's own, from the proposal's last save.
|
|
9684
|
+
if (/^\/publish-jobs\/[A-Za-z0-9_-]{1,100}\/merge-prepare$/.test(path)) {
|
|
9685
|
+
return true;
|
|
9686
|
+
}
|
|
9471
9687
|
if (path === "/build-target" || path === "/project-source" || path === "/update-target" || path === "/publish" ||
|
|
9472
9688
|
// Where "View run" goes, for a connected build CI reported failed.
|
|
9473
9689
|
// Read only: reporting a run is CI's, with its own token.
|
|
@@ -9490,6 +9706,20 @@ class ValOpsHttp extends ValOps {
|
|
|
9490
9706
|
return /^\/publish\/[A-Za-z0-9_-]+(\/(artifacts|verify|promote))?$/.test(path);
|
|
9491
9707
|
}
|
|
9492
9708
|
async publishApi(path, init) {
|
|
9709
|
+
/*
|
|
9710
|
+
* Nothing is published from a proposal's address: its way to the site is
|
|
9711
|
+
* a merge. The publish API reaches the SITE's builds and jobs, so a
|
|
9712
|
+
* Studio there that asked would be publishing over the site.
|
|
9713
|
+
*/
|
|
9714
|
+
if (this.proposal !== null && !ValOpsHttp.mergeApiPathAllowed(init.method, path)) {
|
|
9715
|
+
return {
|
|
9716
|
+
status: 403,
|
|
9717
|
+
contentType: "application/json",
|
|
9718
|
+
body: JSON.stringify({
|
|
9719
|
+
message: "This is a proposal's address: nothing is published from here. " + "A proposal reaches the site by being merged."
|
|
9720
|
+
})
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9493
9723
|
if (!ValOpsHttp.publishApiPathAllowed(path)) {
|
|
9494
9724
|
return {
|
|
9495
9725
|
status: 403,
|
|
@@ -9501,6 +9731,82 @@ class ValOpsHttp extends ValOps {
|
|
|
9501
9731
|
}
|
|
9502
9732
|
return this.callPublishApi(path, init);
|
|
9503
9733
|
}
|
|
9734
|
+
currentProposal() {
|
|
9735
|
+
return this.proposal === null ? null : {
|
|
9736
|
+
name: this.proposal.name,
|
|
9737
|
+
branch: this.proposal.branch
|
|
9738
|
+
};
|
|
9739
|
+
}
|
|
9740
|
+
|
|
9741
|
+
/**
|
|
9742
|
+
* Which proposal calls the browser may make, by method and the path after
|
|
9743
|
+
* `/proposals`: list and open, read and rename one, and close, reopen and
|
|
9744
|
+
* retry its setup. Not `/save` -- the Studio saves through `/save`, which
|
|
9745
|
+
* prepares the commit here -- and not `/patches`, which moves the site's
|
|
9746
|
+
* changes into a proposal and has no screen yet.
|
|
9747
|
+
*/
|
|
9748
|
+
static proposalsApiAllowed(method, path) {
|
|
9749
|
+
if (path === "") return method === "GET" || method === "POST";
|
|
9750
|
+
const one = /^\/[0-9a-f]{20}$/.test(path);
|
|
9751
|
+
if (one) return method === "GET" || method === "PATCH";
|
|
9752
|
+
if (method === "GET") return /^\/[0-9a-f]{20}\/merge-checks$/.test(path);
|
|
9753
|
+
return method === "POST" && /^\/[0-9a-f]{20}\/(close|reopen|setup\/retry|merge)$/.test(path);
|
|
9754
|
+
}
|
|
9755
|
+
|
|
9756
|
+
/**
|
|
9757
|
+
* What a proposal's address may reach of the publish API: building ITS OWN
|
|
9758
|
+
* merge, and nothing that publishes the site's changes. The merge's prepare
|
|
9759
|
+
* (content's, from the proposal's last save), the build it needs -- the
|
|
9760
|
+
* site's build target and source, declaring and uploading the build -- and
|
|
9761
|
+
* the job's own steps. Not a press of the site's Publish, not `next` (which
|
|
9762
|
+
* would hand this tab a site job), and not the site job's prepare.
|
|
9763
|
+
*/
|
|
9764
|
+
static mergeApiPathAllowed(method, path) {
|
|
9765
|
+
if (path === "/build-target" || path === "/project-source" || path === "/publish") {
|
|
9766
|
+
return true;
|
|
9767
|
+
}
|
|
9768
|
+
if (/^\/publish\/[A-Za-z0-9_-]+\/artifacts$/.test(path)) return true;
|
|
9769
|
+
// Where its press is -- read only: a POST there is the site's Try again.
|
|
9770
|
+
if (method === "GET" && path !== "/publish-requests/try-again" && /^\/publish-requests\/[A-Za-z0-9_-]{1,100}$/.test(path)) return true;
|
|
9771
|
+
return /^\/publish-jobs\/[A-Za-z0-9_-]{1,100}\/(merge-prepare|steps|renew|cancel)$/.test(path);
|
|
9772
|
+
}
|
|
9773
|
+
async proposalsApi(path, init, profileId) {
|
|
9774
|
+
const json = "application/json";
|
|
9775
|
+
if (!ValOpsHttp.proposalsApiAllowed(init.method, path)) {
|
|
9776
|
+
return {
|
|
9777
|
+
status: 403,
|
|
9778
|
+
contentType: json,
|
|
9779
|
+
body: JSON.stringify({
|
|
9780
|
+
message: `'${init.method} ${path}' is not part of the proposals API.`
|
|
9781
|
+
})
|
|
9782
|
+
};
|
|
9783
|
+
}
|
|
9784
|
+
const res = await fetch(`${this.contentUrl}/v1/${this.project}/proposals${path}`, {
|
|
9785
|
+
method: init.method,
|
|
9786
|
+
headers: {
|
|
9787
|
+
...this.authHeaders,
|
|
9788
|
+
/*
|
|
9789
|
+
* WHO is asking, as for a commit: the app's key names the project,
|
|
9790
|
+
* and a proposal is opened, renamed and closed by a person -- content
|
|
9791
|
+
* refuses those writes from a credential that names nobody.
|
|
9792
|
+
*/
|
|
9793
|
+
...(profileId !== null ? {
|
|
9794
|
+
"x-val-profile-id": profileId
|
|
9795
|
+
} : {}),
|
|
9796
|
+
...(init.body === undefined ? {} : {
|
|
9797
|
+
"Content-Type": json
|
|
9798
|
+
})
|
|
9799
|
+
},
|
|
9800
|
+
...(init.body === undefined ? {} : {
|
|
9801
|
+
body: init.body
|
|
9802
|
+
})
|
|
9803
|
+
});
|
|
9804
|
+
return {
|
|
9805
|
+
status: res.status,
|
|
9806
|
+
body: await res.text(),
|
|
9807
|
+
contentType: res.headers.get("Content-Type") ?? json
|
|
9808
|
+
};
|
|
9809
|
+
}
|
|
9504
9810
|
|
|
9505
9811
|
/**
|
|
9506
9812
|
* A publish job's prepare, sent to content: the job's sources, archived
|
|
@@ -9801,6 +10107,42 @@ class ValOpsHttp extends ValOps {
|
|
|
9801
10107
|
} : {})
|
|
9802
10108
|
};
|
|
9803
10109
|
}
|
|
10110
|
+
|
|
10111
|
+
/**
|
|
10112
|
+
* Where this server sits in the content service's chain, as every request
|
|
10113
|
+
* that asks about a position says it.
|
|
10114
|
+
*
|
|
10115
|
+
* In a proposal: its branch, and the save its snapshot is (none before the
|
|
10116
|
+
* first). The content service then answers with what came after that save
|
|
10117
|
+
* and nothing else -- never the site's position, which would replay the
|
|
10118
|
+
* proposal's own saves over a snapshot that already holds them. Otherwise
|
|
10119
|
+
* the commit this build was made from, or, for a build a tab made, its job.
|
|
10120
|
+
*/
|
|
10121
|
+
position() {
|
|
10122
|
+
if (this.proposal !== null) {
|
|
10123
|
+
return {
|
|
10124
|
+
branch: this.proposal.branch,
|
|
10125
|
+
...(this.proposal.commit !== null ? {
|
|
10126
|
+
commit: this.proposal.commit
|
|
10127
|
+
} : {})
|
|
10128
|
+
};
|
|
10129
|
+
}
|
|
10130
|
+
if (this.git) {
|
|
10131
|
+
return {
|
|
10132
|
+
branch: this.git.branch,
|
|
10133
|
+
commit: this.git.commit
|
|
10134
|
+
};
|
|
10135
|
+
}
|
|
10136
|
+
return this.publishJob !== null ? {
|
|
10137
|
+
job: this.publishJob
|
|
10138
|
+
} : {};
|
|
10139
|
+
}
|
|
10140
|
+
|
|
10141
|
+
/** The branch this server's patches are on, where it knows it. */
|
|
10142
|
+
ownBranch() {
|
|
10143
|
+
var _this$proposal, _this$git;
|
|
10144
|
+
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);
|
|
10145
|
+
}
|
|
9804
10146
|
async getWebSocketNonce(profileId) {
|
|
9805
10147
|
return fetch(`${this.contentUrl}/v1/${this.project}/websocket/nonces`, {
|
|
9806
10148
|
method: "POST",
|
|
@@ -9812,12 +10154,24 @@ class ValOpsHttp extends ValOps {
|
|
|
9812
10154
|
* the project -- and a nonce is scoped to the project and the person,
|
|
9813
10155
|
* not to a position in a chain.
|
|
9814
10156
|
*/
|
|
9815
|
-
...(
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
10157
|
+
...(() => {
|
|
10158
|
+
const {
|
|
10159
|
+
branch,
|
|
10160
|
+
commit,
|
|
10161
|
+
job
|
|
10162
|
+
} = this.position();
|
|
10163
|
+
return {
|
|
10164
|
+
...(branch !== undefined ? {
|
|
10165
|
+
branch
|
|
10166
|
+
} : {}),
|
|
10167
|
+
...(commit !== undefined ? {
|
|
10168
|
+
commitSha: commit
|
|
10169
|
+
} : {}),
|
|
10170
|
+
...(job !== undefined ? {
|
|
10171
|
+
job
|
|
10172
|
+
} : {})
|
|
10173
|
+
};
|
|
10174
|
+
})(),
|
|
9821
10175
|
// Which build is asking, beside what it says about itself.
|
|
9822
10176
|
...(this.publishBuild !== null ? {
|
|
9823
10177
|
build: this.publishBuild
|
|
@@ -9999,12 +10353,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9999
10353
|
* holds the chain, and for a project it is the only publisher of, the
|
|
10000
10354
|
* position IS the head.
|
|
10001
10355
|
*/
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
// A build a tab made has no commit, and says which job it was made for.
|
|
10007
|
-
params.push(["job", this.publishJob]);
|
|
10356
|
+
// A build a tab made has no commit, and says which job it was made for;
|
|
10357
|
+
// a proposal says its branch and its save. See `position`.
|
|
10358
|
+
for (const [key, value] of Object.entries(this.position())) {
|
|
10359
|
+
if (value !== undefined) params.push([key, value]);
|
|
10008
10360
|
}
|
|
10009
10361
|
/*
|
|
10010
10362
|
* And WHICH build this is, when the platform running it says: the only
|
|
@@ -10277,6 +10629,23 @@ class ValOpsHttp extends ValOps {
|
|
|
10277
10629
|
* cache: a second render a second later asks again.
|
|
10278
10630
|
*/
|
|
10279
10631
|
patchGroupsCache = null;
|
|
10632
|
+
|
|
10633
|
+
/**
|
|
10634
|
+
* Forget the remembered group list, after a write through this server.
|
|
10635
|
+
*
|
|
10636
|
+
* A write changes membership -- a save joins its author's group, a delete or
|
|
10637
|
+
* an unstage leaves it -- and the window above is long enough for the next
|
|
10638
|
+
* render to be served the list from before it: an editor who saved and
|
|
10639
|
+
* reloaded inside a second was shown their draft without the change they had
|
|
10640
|
+
* just made. Called once the write has answered, so a read that started
|
|
10641
|
+
* while it was in flight cannot leave the old list behind it.
|
|
10642
|
+
*
|
|
10643
|
+
* Only this instance's writes. Another instance's write is still seen at
|
|
10644
|
+
* most one window late, which is what the window is for.
|
|
10645
|
+
*/
|
|
10646
|
+
forgetPatchGroups() {
|
|
10647
|
+
this.patchGroupsCache = null;
|
|
10648
|
+
}
|
|
10280
10649
|
async getPatchGroups(options) {
|
|
10281
10650
|
const now = Date.now();
|
|
10282
10651
|
if ((options === null || options === void 0 ? void 0 : options.fresh) !== true && this.patchGroupsCache !== null && now - this.patchGroupsCache.at < PATCH_GROUPS_CACHE_MS) {
|
|
@@ -10308,6 +10677,17 @@ class ValOpsHttp extends ValOps {
|
|
|
10308
10677
|
return res;
|
|
10309
10678
|
}
|
|
10310
10679
|
async fetchPatchGroups() {
|
|
10680
|
+
/*
|
|
10681
|
+
* A proposal has no patch groups: its changes are one unit, saved
|
|
10682
|
+
* together, and the content service answers 404 for groups on its branch.
|
|
10683
|
+
* "Unsupported" is exactly that answer, and it is what makes a draft in a
|
|
10684
|
+
* proposal show all of the proposal's changes rather than one person's.
|
|
10685
|
+
*/
|
|
10686
|
+
if (this.proposal !== null) {
|
|
10687
|
+
return {
|
|
10688
|
+
status: "unsupported"
|
|
10689
|
+
};
|
|
10690
|
+
}
|
|
10311
10691
|
try {
|
|
10312
10692
|
/*
|
|
10313
10693
|
* `branch` is REQUIRED by the endpoint, which answers 400 without it.
|
|
@@ -10316,7 +10696,8 @@ class ValOpsHttp extends ValOps {
|
|
|
10316
10696
|
* `saveSourceFilePatch`): groups are per branch, so a request without one
|
|
10317
10697
|
* is not merely under-specified, it is rejected.
|
|
10318
10698
|
*/
|
|
10319
|
-
const
|
|
10699
|
+
const branch = this.ownBranch();
|
|
10700
|
+
const params = new URLSearchParams(branch !== undefined ? [["branch", branch]] : []);
|
|
10320
10701
|
const res = await fetch(`${this.contentUrl}/v1/${this.project}/patch-groups?${params}`, {
|
|
10321
10702
|
headers: this.authHeaders
|
|
10322
10703
|
});
|
|
@@ -10367,8 +10748,9 @@ class ValOpsHttp extends ValOps {
|
|
|
10367
10748
|
* Without one the content API uses the project's own, as it does for reads.
|
|
10368
10749
|
*/
|
|
10369
10750
|
ownGroupBranch() {
|
|
10370
|
-
|
|
10371
|
-
|
|
10751
|
+
const branch = this.ownBranch();
|
|
10752
|
+
return branch !== undefined ? {
|
|
10753
|
+
branch
|
|
10372
10754
|
} : {};
|
|
10373
10755
|
}
|
|
10374
10756
|
async mutatePatchGroup(method, path, body,
|
|
@@ -10391,6 +10773,22 @@ class ValOpsHttp extends ValOps {
|
|
|
10391
10773
|
* on its own there and this adds nothing.
|
|
10392
10774
|
*/
|
|
10393
10775
|
authorId) {
|
|
10776
|
+
if (this.proposal !== null) {
|
|
10777
|
+
return {
|
|
10778
|
+
status: 409,
|
|
10779
|
+
patchIds: [],
|
|
10780
|
+
error: {
|
|
10781
|
+
message: "A proposal's changes are saved together, so there is nothing to stage or unstage in one."
|
|
10782
|
+
}
|
|
10783
|
+
};
|
|
10784
|
+
}
|
|
10785
|
+
try {
|
|
10786
|
+
return await this.mutatePatchGroupRequest(method, path, body, authorId);
|
|
10787
|
+
} finally {
|
|
10788
|
+
this.forgetPatchGroups();
|
|
10789
|
+
}
|
|
10790
|
+
}
|
|
10791
|
+
async mutatePatchGroupRequest(method, path, body, authorId) {
|
|
10394
10792
|
try {
|
|
10395
10793
|
const res = await fetch(`${this.contentUrl}/v1/${this.project}/${path}`, {
|
|
10396
10794
|
method,
|
|
@@ -10483,7 +10881,13 @@ class ValOpsHttp extends ValOps {
|
|
|
10483
10881
|
patchId,
|
|
10484
10882
|
parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
|
|
10485
10883
|
baseSha,
|
|
10486
|
-
|
|
10884
|
+
/*
|
|
10885
|
+
* In a proposal, its branch alone: the content service files the
|
|
10886
|
+
* patch there, and only for an open proposal.
|
|
10887
|
+
*/
|
|
10888
|
+
...(this.proposal !== null ? {
|
|
10889
|
+
branch: this.proposal.branch
|
|
10890
|
+
} : this.git ? {
|
|
10487
10891
|
commit: this.git.commit,
|
|
10488
10892
|
branch: this.git.branch
|
|
10489
10893
|
} : {}),
|
|
@@ -10560,7 +10964,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10560
10964
|
errorType: "other",
|
|
10561
10965
|
message: `Could save source file patch (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
|
10562
10966
|
});
|
|
10563
|
-
});
|
|
10967
|
+
}).finally(() => this.forgetPatchGroups());
|
|
10564
10968
|
}
|
|
10565
10969
|
|
|
10566
10970
|
/**
|
|
@@ -10852,12 +11256,11 @@ class ValOpsHttp extends ValOps {
|
|
|
10852
11256
|
if (res.ok) {
|
|
10853
11257
|
const parsed = DeletePatchesResponse.safeParse(await res.json());
|
|
10854
11258
|
if (parsed.success) {
|
|
10855
|
-
var _this$git;
|
|
10856
11259
|
const errors = {};
|
|
10857
11260
|
for (const err of parsed.data.errors || []) {
|
|
10858
11261
|
errors[err.patchId] = err;
|
|
10859
11262
|
}
|
|
10860
|
-
const headVersion = ownBranchVersion(parsed.data.headVersions,
|
|
11263
|
+
const headVersion = ownBranchVersion(parsed.data.headVersions, this.ownBranch());
|
|
10861
11264
|
const version = headVersion !== undefined ? {
|
|
10862
11265
|
headVersion
|
|
10863
11266
|
} : {};
|
|
@@ -10890,7 +11293,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10890
11293
|
message: `Could not delete patches (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
|
10891
11294
|
}
|
|
10892
11295
|
};
|
|
10893
|
-
});
|
|
11296
|
+
}).finally(() => this.forgetPatchGroups());
|
|
10894
11297
|
}
|
|
10895
11298
|
async commit(prepared, message, committer, filesDirectory, newBranch,
|
|
10896
11299
|
/**
|
|
@@ -10910,7 +11313,13 @@ class ValOpsHttp extends ValOps {
|
|
|
10910
11313
|
patchGroupId) {
|
|
10911
11314
|
try {
|
|
10912
11315
|
var _res$headers$get3;
|
|
10913
|
-
|
|
11316
|
+
/*
|
|
11317
|
+
* A save in a proposal is a commit on the proposal's branch, at its own
|
|
11318
|
+
* route: the site's `/commit` refuses a proposal's patches, which is
|
|
11319
|
+
* the line that keeps them off the site until a merge.
|
|
11320
|
+
*/
|
|
11321
|
+
const commitUrl = this.proposal !== null ? `${this.contentUrl}/v1/${this.project}/proposals/${this.proposal.name}/save` : `${this.contentUrl}/v1/${this.project}/commit`;
|
|
11322
|
+
const res = await fetch(commitUrl, {
|
|
10914
11323
|
method: "POST",
|
|
10915
11324
|
headers: {
|
|
10916
11325
|
...this.authHeaders,
|
|
@@ -12975,6 +13384,9 @@ async function initHandlerOptions(route, opts, config) {
|
|
|
12975
13384
|
...(opts.publishBuild !== undefined ? {
|
|
12976
13385
|
publishBuild: opts.publishBuild
|
|
12977
13386
|
} : {}),
|
|
13387
|
+
...(opts.proposal !== undefined ? {
|
|
13388
|
+
proposal: opts.proposal
|
|
13389
|
+
} : {}),
|
|
12978
13390
|
project: maybeValProject,
|
|
12979
13391
|
valEnableRedirectUrl,
|
|
12980
13392
|
valDisableRedirectUrl,
|
|
@@ -13050,6 +13462,9 @@ function createValOps(valModules, options) {
|
|
|
13050
13462
|
} : {}),
|
|
13051
13463
|
...(options.publishBuild !== undefined ? {
|
|
13052
13464
|
publishBuild: options.publishBuild
|
|
13465
|
+
} : {}),
|
|
13466
|
+
...(options.proposal !== undefined ? {
|
|
13467
|
+
proposal: options.proposal
|
|
13053
13468
|
} : {})
|
|
13054
13469
|
});
|
|
13055
13470
|
}
|
|
@@ -13648,6 +14063,32 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
13648
14063
|
};
|
|
13649
14064
|
}
|
|
13650
14065
|
};
|
|
14066
|
+
|
|
14067
|
+
/** The person a session names, or `null` for an anonymous one. */
|
|
14068
|
+
const profileOf = auth => "id" in auth && auth.id ? auth.id : null;
|
|
14069
|
+
|
|
14070
|
+
/** One proposals-API call, carried as `proxyPublishApi` carries one. */
|
|
14071
|
+
const proxyProposalsApi = async (path, method, profileId, body) => {
|
|
14072
|
+
const answer = await serverOps.proposalsApi(path ?? "", {
|
|
14073
|
+
method,
|
|
14074
|
+
...(body === undefined ? {} : {
|
|
14075
|
+
body
|
|
14076
|
+
})
|
|
14077
|
+
}, profileId);
|
|
14078
|
+
try {
|
|
14079
|
+
return {
|
|
14080
|
+
status: answer.status,
|
|
14081
|
+
json: JSON.parse(answer.body)
|
|
14082
|
+
};
|
|
14083
|
+
} catch {
|
|
14084
|
+
return {
|
|
14085
|
+
status: answer.status,
|
|
14086
|
+
json: {
|
|
14087
|
+
message: answer.body.trim() === "" ? `The proposals API answered ${answer.status} with no body.` : `The proposals API answered ${answer.status}: ${answer.body.slice(0, 300)}`
|
|
14088
|
+
}
|
|
14089
|
+
};
|
|
14090
|
+
}
|
|
14091
|
+
};
|
|
13651
14092
|
return {
|
|
13652
14093
|
"/draft/enable": {
|
|
13653
14094
|
GET: async req => {
|
|
@@ -14081,6 +14522,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14081
14522
|
* shows one story to a project that lives by the other.
|
|
14082
14523
|
*/
|
|
14083
14524
|
const sourceMode = serverOps.sourceMode();
|
|
14525
|
+
// Spread like the two above: absent is "on the site".
|
|
14526
|
+
const proposal = serverOps.currentProposal();
|
|
14084
14527
|
return {
|
|
14085
14528
|
status: 200,
|
|
14086
14529
|
json: {
|
|
@@ -14096,6 +14539,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14096
14539
|
...(sourceMode ? {
|
|
14097
14540
|
publishJobs: serverOps.publishesAsJobs()
|
|
14098
14541
|
} : {}),
|
|
14542
|
+
...(proposal ? {
|
|
14543
|
+
proposal
|
|
14544
|
+
} : {}),
|
|
14099
14545
|
// Not `options.config` verbatim: in proxy mode the branch the
|
|
14100
14546
|
// server resolved is filled in where the file did not name one.
|
|
14101
14547
|
// See `clientConfig`.
|
|
@@ -14145,6 +14591,78 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14145
14591
|
return proxyPublishApi(req.path, "POST", req.body === undefined ? undefined : JSON.stringify(req.body));
|
|
14146
14592
|
}
|
|
14147
14593
|
},
|
|
14594
|
+
/**
|
|
14595
|
+
* The content service's proposals API, reached through this deployment --
|
|
14596
|
+
* the switcher, the list and the dialogs. The session is checked and its
|
|
14597
|
+
* profile sent along, because a proposal is changed by a person; see
|
|
14598
|
+
* `ValOpsHttp.proposalsApi` for what may be reached.
|
|
14599
|
+
*/
|
|
14600
|
+
"/proposals-api": {
|
|
14601
|
+
GET: async req => {
|
|
14602
|
+
const auth = getAuth(req.cookies);
|
|
14603
|
+
if (auth.error) {
|
|
14604
|
+
return {
|
|
14605
|
+
status: 401,
|
|
14606
|
+
json: {
|
|
14607
|
+
message: auth.error
|
|
14608
|
+
}
|
|
14609
|
+
};
|
|
14610
|
+
}
|
|
14611
|
+
return proxyProposalsApi(req.path, "GET", profileOf(auth));
|
|
14612
|
+
},
|
|
14613
|
+
POST: async req => {
|
|
14614
|
+
const auth = getAuth(req.cookies);
|
|
14615
|
+
if (auth.error) {
|
|
14616
|
+
return {
|
|
14617
|
+
status: 401,
|
|
14618
|
+
json: {
|
|
14619
|
+
message: auth.error
|
|
14620
|
+
}
|
|
14621
|
+
};
|
|
14622
|
+
}
|
|
14623
|
+
return proxyProposalsApi(req.path, "POST", profileOf(auth), JSON.stringify(req.body ?? {}));
|
|
14624
|
+
},
|
|
14625
|
+
PATCH: async req => {
|
|
14626
|
+
const auth = getAuth(req.cookies);
|
|
14627
|
+
if (auth.error) {
|
|
14628
|
+
return {
|
|
14629
|
+
status: 401,
|
|
14630
|
+
json: {
|
|
14631
|
+
message: auth.error
|
|
14632
|
+
}
|
|
14633
|
+
};
|
|
14634
|
+
}
|
|
14635
|
+
return proxyProposalsApi(req.path, "PATCH", profileOf(auth), JSON.stringify(req.body ?? {}));
|
|
14636
|
+
}
|
|
14637
|
+
},
|
|
14638
|
+
"/proposal-site-sources": {
|
|
14639
|
+
GET: async req => {
|
|
14640
|
+
const auth = getAuth(req.cookies);
|
|
14641
|
+
if (auth.error) {
|
|
14642
|
+
return {
|
|
14643
|
+
status: 401,
|
|
14644
|
+
json: {
|
|
14645
|
+
message: auth.error
|
|
14646
|
+
}
|
|
14647
|
+
};
|
|
14648
|
+
}
|
|
14649
|
+
const modules = await serverOps.siteSourcesUnderSnapshot();
|
|
14650
|
+
if (modules === null) {
|
|
14651
|
+
return {
|
|
14652
|
+
status: 404,
|
|
14653
|
+
json: {
|
|
14654
|
+
message: "This is not a proposal's address."
|
|
14655
|
+
}
|
|
14656
|
+
};
|
|
14657
|
+
}
|
|
14658
|
+
return {
|
|
14659
|
+
status: 200,
|
|
14660
|
+
json: {
|
|
14661
|
+
modules
|
|
14662
|
+
}
|
|
14663
|
+
};
|
|
14664
|
+
}
|
|
14665
|
+
},
|
|
14148
14666
|
"/upload/patches": {
|
|
14149
14667
|
POST: async req => {
|
|
14150
14668
|
if (serverOps instanceof ValOpsHttp) {
|
|
@@ -15123,9 +15641,23 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15123
15641
|
patchIds: undefined,
|
|
15124
15642
|
excludePatchOps: false
|
|
15125
15643
|
});
|
|
15644
|
+
/*
|
|
15645
|
+
* Membership from THIS response when it carries one, not from the
|
|
15646
|
+
* group list `resolveOwnPatchScope` read.
|
|
15647
|
+
*
|
|
15648
|
+
* That list is a separate request, remembered for a second
|
|
15649
|
+
* (`ValOpsHttp.getPatchGroups`), so it can be older than the chain
|
|
15650
|
+
* fetched here: an editor who saved and reloaded at once was
|
|
15651
|
+
* rendered without the patch they had just written, because the
|
|
15652
|
+
* list still had their group without it. The content service reads
|
|
15653
|
+
* the chain and each patch's groups in one transaction, and
|
|
15654
|
+
* `fetchPatches` folds that onto `patchGroups` -- so the patches
|
|
15655
|
+
* and who owns them come from the same moment.
|
|
15656
|
+
*/
|
|
15657
|
+
const own = ownPatchScopeFor(all, "id" in auth && auth.id || undefined, ownPatchIds);
|
|
15126
15658
|
patchOps = {
|
|
15127
15659
|
...all,
|
|
15128
|
-
patches: scopedPatches(all.patches,
|
|
15660
|
+
patches: scopedPatches(all.patches, own)
|
|
15129
15661
|
};
|
|
15130
15662
|
} else if (requestedPatchIds !== undefined && requestedPatchIds.length === 0) {
|
|
15131
15663
|
/*
|
|
@@ -17098,6 +17630,24 @@ chain, deleted, requested) {
|
|
|
17098
17630
|
* `undefined` means "apply everything", which is what every caller that does
|
|
17099
17631
|
* not ask for scoping gets and must keep getting.
|
|
17100
17632
|
*/
|
|
17633
|
+
/** The patches in this author's OPEN groups: what a scoped draft shows. */
|
|
17634
|
+
function ownPatchIdsIn(patchGroups, authorId) {
|
|
17635
|
+
return patchGroups.filter(group => group.publishedAt === null && group.authorId === authorId).flatMap(group => group.patchIds);
|
|
17636
|
+
}
|
|
17637
|
+
|
|
17638
|
+
/**
|
|
17639
|
+
* Whose pending patches a scoped draft shows, taken from the SAME response as
|
|
17640
|
+
* the patches when it says, and from the list read beforehand only when not.
|
|
17641
|
+
*
|
|
17642
|
+
* See the call in `/sources/~`: the list read beforehand can be older than the
|
|
17643
|
+
* chain, and the response's membership cannot be.
|
|
17644
|
+
*/
|
|
17645
|
+
function ownPatchScopeFor(fetched, authorId, remembered) {
|
|
17646
|
+
if (fetched.patchGroups === undefined || authorId === undefined) {
|
|
17647
|
+
return remembered;
|
|
17648
|
+
}
|
|
17649
|
+
return ownPatchIdsIn(fetched.patchGroups, authorId);
|
|
17650
|
+
}
|
|
17101
17651
|
async function resolveOwnPatchScope(
|
|
17102
17652
|
/*
|
|
17103
17653
|
* `ValOps`, not the two concrete stores: the one thing this needs is whether
|
|
@@ -17152,7 +17702,7 @@ serverOps, opts) {
|
|
|
17152
17702
|
* nothing, and base is the honest answer. Distinct from the
|
|
17153
17703
|
* case above, which is why the two are not one expression.
|
|
17154
17704
|
*/
|
|
17155
|
-
ownPatchIds = groupsRes.patchGroups
|
|
17705
|
+
ownPatchIds = ownPatchIdsIn(groupsRes.patchGroups, opts.authorId);
|
|
17156
17706
|
/*
|
|
17157
17707
|
* Scoping applies to PENDING work only. Anything already
|
|
17158
17708
|
* committed is part of everyone's view.
|
|
@@ -23255,6 +23805,8 @@ exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;
|
|
|
23255
23805
|
exports.patchSourceFile = patchSourceFile;
|
|
23256
23806
|
exports.persistPersonalAccessToken = persistPersonalAccessToken;
|
|
23257
23807
|
exports.planJsonValuesEntryExtraction = planJsonValuesEntryExtraction;
|
|
23808
|
+
exports.proposalFromEnv = proposalFromEnv;
|
|
23809
|
+
exports.proposalSnapshot = proposalSnapshot;
|
|
23258
23810
|
exports.readCapturedReport = readCapturedReport;
|
|
23259
23811
|
exports.readPatchStore = readPatchStore;
|
|
23260
23812
|
exports.rebaseContentOp = rebaseContentOp;
|