@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.
- package/CHANGELOG.md +33 -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 +615 -37
- package/dist/valbuild-server.cjs.prod.js +615 -37
- package/dist/valbuild-server.esm.js +614 -38
- package/package.json +3 -3
|
@@ -2498,6 +2498,24 @@ function defineExternal(definition) {
|
|
|
2498
2498
|
throw new Error("defineExternal is not implemented yet: phase 0 lands the contract, the registry that executes it arrives with the read endpoints.");
|
|
2499
2499
|
}
|
|
2500
2500
|
|
|
2501
|
+
/**
|
|
2502
|
+
* Does saving this commit upload a file to Val's remote host?
|
|
2503
|
+
*
|
|
2504
|
+
* The question `/save` asks before it reads remote credentials. It used to ask
|
|
2505
|
+
* the SCHEMAS instead -- "is any of them remote?" -- and with
|
|
2506
|
+
* `files: { remote: true }` every media schema is, so every save needed a
|
|
2507
|
+
* `val login` token or an api key: a corrected typo included. Only a remote
|
|
2508
|
+
* FILE is uploaded, so only a commit carrying one needs credentials.
|
|
2509
|
+
*/
|
|
2510
|
+
function commitCarriesRemoteFiles(preparedCommit) {
|
|
2511
|
+
return Object.values(preparedCommit.patchedBinaryFilesDescriptors).some(descriptor => descriptor.remote);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/** Does this commit carry any file at all, remote or local? */
|
|
2515
|
+
function commitCarriesFiles(preparedCommit) {
|
|
2516
|
+
return Object.keys(preparedCommit.patchedBinaryFilesDescriptors).length > 0;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2501
2519
|
const JwtPayloadSchema = z.z.object({
|
|
2502
2520
|
sub: z.z.string(),
|
|
2503
2521
|
exp: z.z.number(),
|
|
@@ -2698,18 +2716,41 @@ function encodeJwt(payload, sessionKey) {
|
|
|
2698
2716
|
* error somebody had to answer instead of a default that silently applied.
|
|
2699
2717
|
*/
|
|
2700
2718
|
function clientConfig(options) {
|
|
2719
|
+
const config = withResolvedProject(options.config, options.project);
|
|
2701
2720
|
if (options.mode !== "http" || options.git === undefined) {
|
|
2702
|
-
return
|
|
2721
|
+
return config;
|
|
2703
2722
|
}
|
|
2704
|
-
if (
|
|
2705
|
-
return
|
|
2723
|
+
if (config.gitBranch !== undefined) {
|
|
2724
|
+
return config;
|
|
2706
2725
|
}
|
|
2707
2726
|
return {
|
|
2708
|
-
...
|
|
2727
|
+
...config,
|
|
2709
2728
|
gitBranch: options.git.branch
|
|
2710
2729
|
};
|
|
2711
2730
|
}
|
|
2712
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
|
+
|
|
2713
2754
|
/**
|
|
2714
2755
|
* Which IP addresses this server may open a connection to.
|
|
2715
2756
|
*
|
|
@@ -3583,6 +3624,12 @@ class ValOps {
|
|
|
3583
3624
|
* hash. Re-folding with the wrong list changes the base SHA for no reason.
|
|
3584
3625
|
*/
|
|
3585
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;
|
|
3586
3633
|
/**
|
|
3587
3634
|
* What a save has told us each `.jsonValues()` entry now holds.
|
|
3588
3635
|
*
|
|
@@ -3629,6 +3676,7 @@ class ValOps {
|
|
|
3629
3676
|
// #region initTree
|
|
3630
3677
|
async initSources() {
|
|
3631
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;
|
|
3632
3680
|
const extracted = await core.extractValModules(this.valModules);
|
|
3633
3681
|
const moduleErrors = extracted.moduleErrors.concat(findNestedJsonValuesModuleErrors(extracted.schemas));
|
|
3634
3682
|
this.sources = extracted.sources;
|
|
@@ -3640,12 +3688,29 @@ class ValOps {
|
|
|
3640
3688
|
this.modulesErrors = moduleErrors;
|
|
3641
3689
|
this.shaEntries = extracted.shaEntries;
|
|
3642
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
|
+
}
|
|
3643
3708
|
return {
|
|
3644
3709
|
baseSha: this.baseSha,
|
|
3645
3710
|
schemaSha: this.schemaSha,
|
|
3646
3711
|
sourcesSha: this.sourcesSha,
|
|
3647
3712
|
configSha: this.configSha,
|
|
3648
|
-
sources:
|
|
3713
|
+
sources: this.sources,
|
|
3649
3714
|
schemas: extracted.schemas,
|
|
3650
3715
|
moduleErrors
|
|
3651
3716
|
};
|
|
@@ -3661,6 +3726,19 @@ class ValOps {
|
|
|
3661
3726
|
};
|
|
3662
3727
|
}
|
|
3663
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
|
+
|
|
3664
3742
|
/**
|
|
3665
3743
|
* These patches are on disk now: adopt what they produced as the committed
|
|
3666
3744
|
* sources.
|
|
@@ -5257,9 +5335,9 @@ class ValOps {
|
|
|
5257
5335
|
// https://github.com/microsoft/TypeScript/issues/36174
|
|
5258
5336
|
let sourceFileText = null;
|
|
5259
5337
|
if (tsChanged) {
|
|
5260
|
-
var _this$
|
|
5338
|
+
var _this$options3;
|
|
5261
5339
|
sourceFileText = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
|
|
5262
|
-
if ((_this$
|
|
5340
|
+
if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.formatter) {
|
|
5263
5341
|
try {
|
|
5264
5342
|
sourceFileText = await this.options.formatter(sourceFileText, path);
|
|
5265
5343
|
} catch (err) {
|
|
@@ -5271,13 +5349,13 @@ class ValOps {
|
|
|
5271
5349
|
}
|
|
5272
5350
|
const extraFiles = {};
|
|
5273
5351
|
for (const [jsonPath, content] of Array.from(jsonEntryContents)) {
|
|
5274
|
-
var _this$
|
|
5352
|
+
var _this$options4;
|
|
5275
5353
|
if (content === null) {
|
|
5276
5354
|
extraFiles[jsonPath] = null;
|
|
5277
5355
|
continue;
|
|
5278
5356
|
}
|
|
5279
5357
|
let jsonText = JSON.stringify(content, null, 2);
|
|
5280
|
-
if ((_this$
|
|
5358
|
+
if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.formatter) {
|
|
5281
5359
|
try {
|
|
5282
5360
|
jsonText = await this.options.formatter(jsonText, jsonPath);
|
|
5283
5361
|
} catch (err) {
|
|
@@ -5593,6 +5671,30 @@ class ValOps {
|
|
|
5593
5671
|
};
|
|
5594
5672
|
}
|
|
5595
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
|
+
|
|
5596
5698
|
/**
|
|
5597
5699
|
* Whether a commit here produces `.val.ts` TEXT as well as data.
|
|
5598
5700
|
*
|
|
@@ -8796,6 +8898,111 @@ function computePatchesToDrop(prepared) {
|
|
|
8796
8898
|
return dropped;
|
|
8797
8899
|
}
|
|
8798
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
|
+
|
|
8799
9006
|
const textEncoder$2 = new TextEncoder();
|
|
8800
9007
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
8801
9008
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -9149,6 +9356,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9149
9356
|
publishJob;
|
|
9150
9357
|
/** See `publishBuild` on {@link ValApiOptions}; sent with every position. */
|
|
9151
9358
|
publishBuild;
|
|
9359
|
+
/** See `proposal` on {@link ValApiOptions}; `null` on the site. */
|
|
9360
|
+
proposal;
|
|
9152
9361
|
|
|
9153
9362
|
/** Did the host hand over the running build's source? See `projectSource`. */
|
|
9154
9363
|
embedsSource() {
|
|
@@ -9221,7 +9430,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9221
9430
|
* getSettings / uploadRemoteFile / getPresignedAuthNonce.
|
|
9222
9431
|
*/
|
|
9223
9432
|
auth, valModules, options) {
|
|
9224
|
-
super(valModules, options
|
|
9433
|
+
super(valModules, options !== null && options !== void 0 && options.proposal ? {
|
|
9434
|
+
...options,
|
|
9435
|
+
snapshotSources: proposalSnapshot(options.proposal)
|
|
9436
|
+
} : options);
|
|
9225
9437
|
this.contentUrl = contentUrl;
|
|
9226
9438
|
this.project = project;
|
|
9227
9439
|
this.git = git;
|
|
@@ -9231,7 +9443,15 @@ class ValOpsHttp extends ValOps {
|
|
|
9231
9443
|
Authorization: `Bearer ${auth.apiKey}`
|
|
9232
9444
|
};
|
|
9233
9445
|
this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
|
|
9234
|
-
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;
|
|
9235
9455
|
this.publishJob = (options === null || options === void 0 ? void 0 : options.publishJob) ?? null;
|
|
9236
9456
|
this.publishBuild = (options === null || options === void 0 ? void 0 : options.publishBuild) ?? null;
|
|
9237
9457
|
this.mirrorsSourceFiles = git !== null || this.projectSource !== null;
|
|
@@ -9260,7 +9480,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9260
9480
|
*/
|
|
9261
9481
|
publishRefusal() {
|
|
9262
9482
|
var _this$projectExpectat;
|
|
9263
|
-
|
|
9483
|
+
// A save in a proposal mirrors nothing: it reaches git by merging.
|
|
9484
|
+
if (this.git !== null || this.proposal !== null) {
|
|
9264
9485
|
return null;
|
|
9265
9486
|
}
|
|
9266
9487
|
/*
|
|
@@ -9297,6 +9518,13 @@ class ValOpsHttp extends ValOps {
|
|
|
9297
9518
|
*/
|
|
9298
9519
|
sourceMode() {
|
|
9299
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;
|
|
9300
9528
|
return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
|
|
9301
9529
|
}
|
|
9302
9530
|
|
|
@@ -9331,6 +9559,8 @@ class ValOpsHttp extends ValOps {
|
|
|
9331
9559
|
}).catch(() => undefined);
|
|
9332
9560
|
}
|
|
9333
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;
|
|
9334
9564
|
const expected = this.projectExpectation;
|
|
9335
9565
|
if (expected === null) return false;
|
|
9336
9566
|
return expected.publishJobs ?? expected.sourceMode === "managed";
|
|
@@ -9450,6 +9680,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9450
9680
|
* segment gets through.
|
|
9451
9681
|
*/
|
|
9452
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
|
+
}
|
|
9453
9687
|
if (path === "/build-target" || path === "/project-source" || path === "/update-target" || path === "/publish" ||
|
|
9454
9688
|
// Where "View run" goes, for a connected build CI reported failed.
|
|
9455
9689
|
// Read only: reporting a run is CI's, with its own token.
|
|
@@ -9472,6 +9706,20 @@ class ValOpsHttp extends ValOps {
|
|
|
9472
9706
|
return /^\/publish\/[A-Za-z0-9_-]+(\/(artifacts|verify|promote))?$/.test(path);
|
|
9473
9707
|
}
|
|
9474
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
|
+
}
|
|
9475
9723
|
if (!ValOpsHttp.publishApiPathAllowed(path)) {
|
|
9476
9724
|
return {
|
|
9477
9725
|
status: 403,
|
|
@@ -9483,6 +9731,82 @@ class ValOpsHttp extends ValOps {
|
|
|
9483
9731
|
}
|
|
9484
9732
|
return this.callPublishApi(path, init);
|
|
9485
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
|
+
}
|
|
9486
9810
|
|
|
9487
9811
|
/**
|
|
9488
9812
|
* A publish job's prepare, sent to content: the job's sources, archived
|
|
@@ -9783,6 +10107,42 @@ class ValOpsHttp extends ValOps {
|
|
|
9783
10107
|
} : {})
|
|
9784
10108
|
};
|
|
9785
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
|
+
}
|
|
9786
10146
|
async getWebSocketNonce(profileId) {
|
|
9787
10147
|
return fetch(`${this.contentUrl}/v1/${this.project}/websocket/nonces`, {
|
|
9788
10148
|
method: "POST",
|
|
@@ -9794,12 +10154,24 @@ class ValOpsHttp extends ValOps {
|
|
|
9794
10154
|
* the project -- and a nonce is scoped to the project and the person,
|
|
9795
10155
|
* not to a position in a chain.
|
|
9796
10156
|
*/
|
|
9797
|
-
...(
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
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
|
+
})(),
|
|
9803
10175
|
// Which build is asking, beside what it says about itself.
|
|
9804
10176
|
...(this.publishBuild !== null ? {
|
|
9805
10177
|
build: this.publishBuild
|
|
@@ -9981,12 +10353,10 @@ class ValOpsHttp extends ValOps {
|
|
|
9981
10353
|
* holds the chain, and for a project it is the only publisher of, the
|
|
9982
10354
|
* position IS the head.
|
|
9983
10355
|
*/
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
// A build a tab made has no commit, and says which job it was made for.
|
|
9989
|
-
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]);
|
|
9990
10360
|
}
|
|
9991
10361
|
/*
|
|
9992
10362
|
* And WHICH build this is, when the platform running it says: the only
|
|
@@ -10259,6 +10629,23 @@ class ValOpsHttp extends ValOps {
|
|
|
10259
10629
|
* cache: a second render a second later asks again.
|
|
10260
10630
|
*/
|
|
10261
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
|
+
}
|
|
10262
10649
|
async getPatchGroups(options) {
|
|
10263
10650
|
const now = Date.now();
|
|
10264
10651
|
if ((options === null || options === void 0 ? void 0 : options.fresh) !== true && this.patchGroupsCache !== null && now - this.patchGroupsCache.at < PATCH_GROUPS_CACHE_MS) {
|
|
@@ -10290,6 +10677,17 @@ class ValOpsHttp extends ValOps {
|
|
|
10290
10677
|
return res;
|
|
10291
10678
|
}
|
|
10292
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
|
+
}
|
|
10293
10691
|
try {
|
|
10294
10692
|
/*
|
|
10295
10693
|
* `branch` is REQUIRED by the endpoint, which answers 400 without it.
|
|
@@ -10298,7 +10696,8 @@ class ValOpsHttp extends ValOps {
|
|
|
10298
10696
|
* `saveSourceFilePatch`): groups are per branch, so a request without one
|
|
10299
10697
|
* is not merely under-specified, it is rejected.
|
|
10300
10698
|
*/
|
|
10301
|
-
const
|
|
10699
|
+
const branch = this.ownBranch();
|
|
10700
|
+
const params = new URLSearchParams(branch !== undefined ? [["branch", branch]] : []);
|
|
10302
10701
|
const res = await fetch(`${this.contentUrl}/v1/${this.project}/patch-groups?${params}`, {
|
|
10303
10702
|
headers: this.authHeaders
|
|
10304
10703
|
});
|
|
@@ -10349,8 +10748,9 @@ class ValOpsHttp extends ValOps {
|
|
|
10349
10748
|
* Without one the content API uses the project's own, as it does for reads.
|
|
10350
10749
|
*/
|
|
10351
10750
|
ownGroupBranch() {
|
|
10352
|
-
|
|
10353
|
-
|
|
10751
|
+
const branch = this.ownBranch();
|
|
10752
|
+
return branch !== undefined ? {
|
|
10753
|
+
branch
|
|
10354
10754
|
} : {};
|
|
10355
10755
|
}
|
|
10356
10756
|
async mutatePatchGroup(method, path, body,
|
|
@@ -10373,6 +10773,22 @@ class ValOpsHttp extends ValOps {
|
|
|
10373
10773
|
* on its own there and this adds nothing.
|
|
10374
10774
|
*/
|
|
10375
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) {
|
|
10376
10792
|
try {
|
|
10377
10793
|
const res = await fetch(`${this.contentUrl}/v1/${this.project}/${path}`, {
|
|
10378
10794
|
method,
|
|
@@ -10465,7 +10881,13 @@ class ValOpsHttp extends ValOps {
|
|
|
10465
10881
|
patchId,
|
|
10466
10882
|
parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
|
|
10467
10883
|
baseSha,
|
|
10468
|
-
|
|
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 ? {
|
|
10469
10891
|
commit: this.git.commit,
|
|
10470
10892
|
branch: this.git.branch
|
|
10471
10893
|
} : {}),
|
|
@@ -10542,7 +10964,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10542
10964
|
errorType: "other",
|
|
10543
10965
|
message: `Could save source file patch (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
|
10544
10966
|
});
|
|
10545
|
-
});
|
|
10967
|
+
}).finally(() => this.forgetPatchGroups());
|
|
10546
10968
|
}
|
|
10547
10969
|
|
|
10548
10970
|
/**
|
|
@@ -10834,12 +11256,11 @@ class ValOpsHttp extends ValOps {
|
|
|
10834
11256
|
if (res.ok) {
|
|
10835
11257
|
const parsed = DeletePatchesResponse.safeParse(await res.json());
|
|
10836
11258
|
if (parsed.success) {
|
|
10837
|
-
var _this$git;
|
|
10838
11259
|
const errors = {};
|
|
10839
11260
|
for (const err of parsed.data.errors || []) {
|
|
10840
11261
|
errors[err.patchId] = err;
|
|
10841
11262
|
}
|
|
10842
|
-
const headVersion = ownBranchVersion(parsed.data.headVersions,
|
|
11263
|
+
const headVersion = ownBranchVersion(parsed.data.headVersions, this.ownBranch());
|
|
10843
11264
|
const version = headVersion !== undefined ? {
|
|
10844
11265
|
headVersion
|
|
10845
11266
|
} : {};
|
|
@@ -10872,7 +11293,7 @@ class ValOpsHttp extends ValOps {
|
|
|
10872
11293
|
message: `Could not delete patches (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
|
10873
11294
|
}
|
|
10874
11295
|
};
|
|
10875
|
-
});
|
|
11296
|
+
}).finally(() => this.forgetPatchGroups());
|
|
10876
11297
|
}
|
|
10877
11298
|
async commit(prepared, message, committer, filesDirectory, newBranch,
|
|
10878
11299
|
/**
|
|
@@ -10892,7 +11313,13 @@ class ValOpsHttp extends ValOps {
|
|
|
10892
11313
|
patchGroupId) {
|
|
10893
11314
|
try {
|
|
10894
11315
|
var _res$headers$get3;
|
|
10895
|
-
|
|
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, {
|
|
10896
11323
|
method: "POST",
|
|
10897
11324
|
headers: {
|
|
10898
11325
|
...this.authHeaders,
|
|
@@ -12957,6 +13384,9 @@ async function initHandlerOptions(route, opts, config) {
|
|
|
12957
13384
|
...(opts.publishBuild !== undefined ? {
|
|
12958
13385
|
publishBuild: opts.publishBuild
|
|
12959
13386
|
} : {}),
|
|
13387
|
+
...(opts.proposal !== undefined ? {
|
|
13388
|
+
proposal: opts.proposal
|
|
13389
|
+
} : {}),
|
|
12960
13390
|
project: maybeValProject,
|
|
12961
13391
|
valEnableRedirectUrl,
|
|
12962
13392
|
valDisableRedirectUrl,
|
|
@@ -13032,6 +13462,9 @@ function createValOps(valModules, options) {
|
|
|
13032
13462
|
} : {}),
|
|
13033
13463
|
...(options.publishBuild !== undefined ? {
|
|
13034
13464
|
publishBuild: options.publishBuild
|
|
13465
|
+
} : {}),
|
|
13466
|
+
...(options.proposal !== undefined ? {
|
|
13467
|
+
proposal: options.proposal
|
|
13035
13468
|
} : {})
|
|
13036
13469
|
});
|
|
13037
13470
|
}
|
|
@@ -13630,6 +14063,32 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
13630
14063
|
};
|
|
13631
14064
|
}
|
|
13632
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
|
+
};
|
|
13633
14092
|
return {
|
|
13634
14093
|
"/draft/enable": {
|
|
13635
14094
|
GET: async req => {
|
|
@@ -14063,6 +14522,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14063
14522
|
* shows one story to a project that lives by the other.
|
|
14064
14523
|
*/
|
|
14065
14524
|
const sourceMode = serverOps.sourceMode();
|
|
14525
|
+
// Spread like the two above: absent is "on the site".
|
|
14526
|
+
const proposal = serverOps.currentProposal();
|
|
14066
14527
|
return {
|
|
14067
14528
|
status: 200,
|
|
14068
14529
|
json: {
|
|
@@ -14078,6 +14539,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14078
14539
|
...(sourceMode ? {
|
|
14079
14540
|
publishJobs: serverOps.publishesAsJobs()
|
|
14080
14541
|
} : {}),
|
|
14542
|
+
...(proposal ? {
|
|
14543
|
+
proposal
|
|
14544
|
+
} : {}),
|
|
14081
14545
|
// Not `options.config` verbatim: in proxy mode the branch the
|
|
14082
14546
|
// server resolved is filled in where the file did not name one.
|
|
14083
14547
|
// See `clientConfig`.
|
|
@@ -14127,6 +14591,78 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14127
14591
|
return proxyPublishApi(req.path, "POST", req.body === undefined ? undefined : JSON.stringify(req.body));
|
|
14128
14592
|
}
|
|
14129
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
|
+
},
|
|
14130
14666
|
"/upload/patches": {
|
|
14131
14667
|
POST: async req => {
|
|
14132
14668
|
if (serverOps instanceof ValOpsHttp) {
|
|
@@ -15105,9 +15641,23 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15105
15641
|
patchIds: undefined,
|
|
15106
15642
|
excludePatchOps: false
|
|
15107
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);
|
|
15108
15658
|
patchOps = {
|
|
15109
15659
|
...all,
|
|
15110
|
-
patches: scopedPatches(all.patches,
|
|
15660
|
+
patches: scopedPatches(all.patches, own)
|
|
15111
15661
|
};
|
|
15112
15662
|
} else if (requestedPatchIds !== undefined && requestedPatchIds.length === 0) {
|
|
15113
15663
|
/*
|
|
@@ -15732,7 +16282,13 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15732
16282
|
* tree; a store with no working tree does only the push.
|
|
15733
16283
|
*/
|
|
15734
16284
|
if (serverOps instanceof ValOpsMemory) {
|
|
15735
|
-
|
|
16285
|
+
/*
|
|
16286
|
+
* A commit with no files uploads nothing and needs no credentials;
|
|
16287
|
+
* see `commitCarriesRemoteFiles`. One with a LOCAL file still goes
|
|
16288
|
+
* through `uploadRemoteFiles` where the project is remote, because
|
|
16289
|
+
* that is where a local file is refused rather than dropped.
|
|
16290
|
+
*/
|
|
16291
|
+
const isRemoteRequired = commitCarriesRemoteFiles(preparedCommit) || commitCarriesFiles(preparedCommit) && getIsRemoteRequired(await serverOps.getSchemas());
|
|
15736
16292
|
if (isRemoteRequired) {
|
|
15737
16293
|
const authRes = await getRemoteFileAuth();
|
|
15738
16294
|
if (authRes.status !== 200) {
|
|
@@ -15755,7 +16311,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15755
16311
|
}
|
|
15756
16312
|
if (serverOps instanceof ValOpsFS) {
|
|
15757
16313
|
var _remoteFileAuthRes;
|
|
15758
|
-
|
|
16314
|
+
// Credentials when THIS save uploads a remote file, not whenever
|
|
16315
|
+
// the project has a remote schema. See `commitCarriesRemoteFiles`.
|
|
16316
|
+
const isRemoteRequired = commitCarriesRemoteFiles(preparedCommit);
|
|
15759
16317
|
let mode;
|
|
15760
16318
|
let remoteFileAuthRes;
|
|
15761
16319
|
if (isRemoteRequired) {
|
|
@@ -17072,6 +17630,24 @@ chain, deleted, requested) {
|
|
|
17072
17630
|
* `undefined` means "apply everything", which is what every caller that does
|
|
17073
17631
|
* not ask for scoping gets and must keep getting.
|
|
17074
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
|
+
}
|
|
17075
17651
|
async function resolveOwnPatchScope(
|
|
17076
17652
|
/*
|
|
17077
17653
|
* `ValOps`, not the two concrete stores: the one thing this needs is whether
|
|
@@ -17126,7 +17702,7 @@ serverOps, opts) {
|
|
|
17126
17702
|
* nothing, and base is the honest answer. Distinct from the
|
|
17127
17703
|
* case above, which is why the two are not one expression.
|
|
17128
17704
|
*/
|
|
17129
|
-
ownPatchIds = groupsRes.patchGroups
|
|
17705
|
+
ownPatchIds = ownPatchIdsIn(groupsRes.patchGroups, opts.authorId);
|
|
17130
17706
|
/*
|
|
17131
17707
|
* Scoping applies to PENDING work only. Anything already
|
|
17132
17708
|
* committed is part of everyone's view.
|
|
@@ -23229,6 +23805,8 @@ exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;
|
|
|
23229
23805
|
exports.patchSourceFile = patchSourceFile;
|
|
23230
23806
|
exports.persistPersonalAccessToken = persistPersonalAccessToken;
|
|
23231
23807
|
exports.planJsonValuesEntryExtraction = planJsonValuesEntryExtraction;
|
|
23808
|
+
exports.proposalFromEnv = proposalFromEnv;
|
|
23809
|
+
exports.proposalSnapshot = proposalSnapshot;
|
|
23232
23810
|
exports.readCapturedReport = readCapturedReport;
|
|
23233
23811
|
exports.readPatchStore = readPatchStore;
|
|
23234
23812
|
exports.rebaseContentOp = rebaseContentOp;
|