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