@valbuild/server 0.144.11 → 0.145.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 +24 -0
- package/dist/declarations/src/ValOps.d.ts +17 -3
- package/dist/valbuild-server.cjs.dev.js +72 -13
- package/dist/valbuild-server.cjs.prod.js +72 -13
- package/dist/valbuild-server.esm.js +73 -14
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @valbuild/server
|
|
2
2
|
|
|
3
|
+
## 0.145.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#859](https://github.com/valbuild/val/pull/859) [`f085be0`](https://github.com/valbuild/val/commit/f085be0eb5bdc195e9d8cdb15dd5f8acfdabe23c) Thanks [@freekh](https://github.com/freekh)! - An edit Val Build refuses to save because of the project's plan — the Free plan's 1,000 changes a week are used, or a Hosted trial has ended — is now kept in the Studio instead of being reverted.
|
|
8
|
+
|
|
9
|
+
- `PUT /patches` answers such a save with a `403` of type `plan-refused`, carrying Val Build's reason and fix as data and which patches of the request it did save. It used to answer `400 patch-error`, which the Studio reads as a bad patch and drops.
|
|
10
|
+
- The Studio keeps the refused edits and any made after them, shows the plan notice with **Upgrade** or **Add a card** beside it, and saves them by itself once the plan allows it: as soon as Val Build says editing is open again (an owner added a card, the week moved on), and otherwise by trying again now and then. A publish or a restore that meets held edits says why, in Val Build's words, instead of suggesting the connection.
|
|
11
|
+
- The MCP write tools answer a refused write as `forbidden`, with Val Build's sentence, instead of `internal`.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`b6cb939`](https://github.com/valbuild/val/commit/b6cb9395d8bdcd5182e9c6ba84f5284930de2fa5), [`f085be0`](https://github.com/valbuild/val/commit/f085be0eb5bdc195e9d8cdb15dd5f8acfdabe23c), [`dda5c52`](https://github.com/valbuild/val/commit/dda5c523698395c6dfb96ae8e39dae04307ec332), [`ac0bb7c`](https://github.com/valbuild/val/commit/ac0bb7cb5a99df022b198326a5f8a0cf61e45388)]:
|
|
16
|
+
- @valbuild/core@0.145.0
|
|
17
|
+
- @valbuild/shared@0.145.0
|
|
18
|
+
- @valbuild/ui@0.145.0
|
|
19
|
+
|
|
20
|
+
## 0.144.12
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [[`2193e3a`](https://github.com/valbuild/val/commit/2193e3ae133cd05dd73d0937dde7c15c88417f71)]:
|
|
25
|
+
- @valbuild/ui@0.144.12
|
|
26
|
+
|
|
3
27
|
## 0.144.11
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
|
@@ -2,7 +2,7 @@ import { MediaSource, FileMetadata, FileSource, ImageMetadata, ModuleFilePath, P
|
|
|
2
2
|
import { result } from "@valbuild/core/fp";
|
|
3
3
|
import { JSONValue, ParentRef, Patch, PatchError } from "@valbuild/core/patch";
|
|
4
4
|
import type { HistoryError } from "./history/HistoryError.js";
|
|
5
|
-
import type { PatchGroupT } from "@valbuild/shared/internal";
|
|
5
|
+
import type { PatchGroupT, PlanRefusalErrorT } from "@valbuild/shared/internal";
|
|
6
6
|
import type { AffectedFile, StoredModuleVersion, CommitPage, CommitPatch, HistoricalCommit } from "./history/types.js";
|
|
7
7
|
import { ValSyntaxError, ValSyntaxErrorTree } from "./patch/ts/syntax.js";
|
|
8
8
|
import { ParentPatchId } from "@valbuild/core";
|
|
@@ -456,7 +456,7 @@ export declare abstract class ValOps {
|
|
|
456
456
|
error: GenericErrorMessage;
|
|
457
457
|
} | {
|
|
458
458
|
errorType: "patch-head-conflict";
|
|
459
|
-
}>>;
|
|
459
|
+
} | PlanRefusedError>>;
|
|
460
460
|
/**
|
|
461
461
|
* Why a publish cannot happen here, or `null` when one can.
|
|
462
462
|
*
|
|
@@ -789,7 +789,21 @@ export type SaveSourceFilePatchResult = result.Result<{
|
|
|
789
789
|
errorType: "other";
|
|
790
790
|
} & GenericErrorMessage) | {
|
|
791
791
|
errorType: "patch-head-conflict";
|
|
792
|
-
}>;
|
|
792
|
+
} | PlanRefusedError>;
|
|
793
|
+
/**
|
|
794
|
+
* The content API refused a write for the project's plan (a 403 that says why
|
|
795
|
+
* as data), as opposed to refusing the patch.
|
|
796
|
+
*
|
|
797
|
+
* Its own error rather than an `other`: an `other` becomes a 400, and the
|
|
798
|
+
* Studio drops a patch it is told is bad. This one is good, and is saved once
|
|
799
|
+
* the plan allows it. `error` has the same shape as `other`'s so a caller that
|
|
800
|
+
* only wants a sentence reads it the same way.
|
|
801
|
+
*/
|
|
802
|
+
export type PlanRefusedError = {
|
|
803
|
+
errorType: "plan-refused";
|
|
804
|
+
error: GenericErrorMessage;
|
|
805
|
+
refusal: PlanRefusalErrorT;
|
|
806
|
+
};
|
|
793
807
|
export type PatchAnalysis = {
|
|
794
808
|
patchesByModule: {
|
|
795
809
|
[path: ModuleFilePath]: {
|
|
@@ -5639,6 +5639,11 @@ class ValOps {
|
|
|
5639
5639
|
patchGroup) {
|
|
5640
5640
|
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId, patchGroup);
|
|
5641
5641
|
if (fp.result.isErr(saveRes)) {
|
|
5642
|
+
if (saveRes.error.errorType === "plan-refused") {
|
|
5643
|
+
// The project's plan, not a failure: the Studio keeps the edit and
|
|
5644
|
+
// sends it again later, so an error line here would be one a minute.
|
|
5645
|
+
return fp.result.err(saveRes.error);
|
|
5646
|
+
}
|
|
5642
5647
|
console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
|
|
5643
5648
|
if (saveRes.error.errorType === "patch-head-conflict") {
|
|
5644
5649
|
return fp.result.err({
|
|
@@ -5965,6 +5970,16 @@ function isFileSource(value) {
|
|
|
5965
5970
|
* after the fact.
|
|
5966
5971
|
*/
|
|
5967
5972
|
|
|
5973
|
+
/**
|
|
5974
|
+
* The content API refused a write for the project's plan (a 403 that says why
|
|
5975
|
+
* as data), as opposed to refusing the patch.
|
|
5976
|
+
*
|
|
5977
|
+
* Its own error rather than an `other`: an `other` becomes a 400, and the
|
|
5978
|
+
* Studio drops a patch it is told is bad. This one is good, and is saved once
|
|
5979
|
+
* the plan allows it. `error` has the same shape as `other`'s so a caller that
|
|
5980
|
+
* only wants a sentence reads it the same way.
|
|
5981
|
+
*/
|
|
5982
|
+
|
|
5968
5983
|
function formatPatchSourceError(error) {
|
|
5969
5984
|
if ("message" in error) {
|
|
5970
5985
|
return error.message;
|
|
@@ -9371,6 +9386,11 @@ function isJsonValue(value) {
|
|
|
9371
9386
|
}
|
|
9372
9387
|
|
|
9373
9388
|
const textEncoder$2 = new TextEncoder();
|
|
9389
|
+
|
|
9390
|
+
/** The body of the content API's 403 for a write the project's plan refuses. */
|
|
9391
|
+
const PlanRefusedBody = z.z.object({
|
|
9392
|
+
error: internal.PlanRefusalError
|
|
9393
|
+
});
|
|
9374
9394
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
9375
9395
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
9376
9396
|
z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -11395,6 +11415,22 @@ class ValOpsHttp extends ValOps {
|
|
|
11395
11415
|
if ((_res$headers$get2 = res.headers.get("Content-Type")) !== null && _res$headers$get2 !== void 0 && _res$headers$get2.includes("application/json")) {
|
|
11396
11416
|
const json = await res.json();
|
|
11397
11417
|
const message = internal.getErrorMessageFromUnknownJson(json, "Unknown error");
|
|
11418
|
+
/*
|
|
11419
|
+
* Refused for the plan: the patch is fine, and the reason comes as
|
|
11420
|
+
* data. Kept apart from every other error, which the Studio is told
|
|
11421
|
+
* is a bad patch and drops. A 403 without a refusal in it (a
|
|
11422
|
+
* credential missing the write scope) is still one of those.
|
|
11423
|
+
*/
|
|
11424
|
+
const refusal = res.status === 403 ? PlanRefusedBody.safeParse(json) : null;
|
|
11425
|
+
if (refusal !== null && refusal !== void 0 && refusal.success) {
|
|
11426
|
+
return fp.result.err({
|
|
11427
|
+
errorType: "plan-refused",
|
|
11428
|
+
error: {
|
|
11429
|
+
message
|
|
11430
|
+
},
|
|
11431
|
+
refusal: refusal.data.error
|
|
11432
|
+
});
|
|
11433
|
+
}
|
|
11398
11434
|
return fp.result.err({
|
|
11399
11435
|
errorType: "other",
|
|
11400
11436
|
message
|
|
@@ -15541,6 +15577,24 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15541
15577
|
* `fs` mode and from an older content API.
|
|
15542
15578
|
*/
|
|
15543
15579
|
let headVersion;
|
|
15580
|
+
/*
|
|
15581
|
+
* What this request has saved so far: the whole answer to a save, and
|
|
15582
|
+
* the saved part of one refused for the plan. One builder for both, so
|
|
15583
|
+
* the client records a partly saved batch exactly as a saved one.
|
|
15584
|
+
*/
|
|
15585
|
+
const savedSoFar = () => ({
|
|
15586
|
+
newPatchIds,
|
|
15587
|
+
parentRef,
|
|
15588
|
+
// Absent rather than null where there are no groups: `fs` mode and
|
|
15589
|
+
// a content API that predates them both answer without one, and the
|
|
15590
|
+
// client reads absence as "staging is not available here".
|
|
15591
|
+
...(patchGroupIdFromStore !== undefined ? {
|
|
15592
|
+
patchGroupId: patchGroupIdFromStore
|
|
15593
|
+
} : {}),
|
|
15594
|
+
...(headVersion !== undefined ? {
|
|
15595
|
+
headVersion
|
|
15596
|
+
} : {})
|
|
15597
|
+
});
|
|
15544
15598
|
for (const patch of patches) {
|
|
15545
15599
|
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId, patchGroup);
|
|
15546
15600
|
if (fp.result.isErr(createPatchRes)) {
|
|
@@ -15552,6 +15606,23 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15552
15606
|
message: "The change was written against a different starting point than the server has."
|
|
15553
15607
|
}
|
|
15554
15608
|
};
|
|
15609
|
+
} else if (createPatchRes.error.errorType === "plan-refused") {
|
|
15610
|
+
/*
|
|
15611
|
+
* Val Build refused it for the plan. Passed on as its 403 rather
|
|
15612
|
+
* than a patch error: the patch is fine and the Studio keeps it,
|
|
15613
|
+
* and the reason travels as data so the fix can sit beside it.
|
|
15614
|
+
* The patches before this one in the batch ARE saved, and are
|
|
15615
|
+
* reported as a 200 would report them.
|
|
15616
|
+
*/
|
|
15617
|
+
return {
|
|
15618
|
+
status: 403,
|
|
15619
|
+
json: {
|
|
15620
|
+
type: "plan-refused",
|
|
15621
|
+
message: createPatchRes.error.error.message,
|
|
15622
|
+
error: createPatchRes.error.refusal,
|
|
15623
|
+
...savedSoFar()
|
|
15624
|
+
}
|
|
15625
|
+
};
|
|
15555
15626
|
} else {
|
|
15556
15627
|
return {
|
|
15557
15628
|
status: 400,
|
|
@@ -15582,19 +15653,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15582
15653
|
}
|
|
15583
15654
|
return {
|
|
15584
15655
|
status: 200,
|
|
15585
|
-
json:
|
|
15586
|
-
newPatchIds,
|
|
15587
|
-
parentRef,
|
|
15588
|
-
// Absent rather than null where there are no groups: `fs` mode and
|
|
15589
|
-
// a content API that predates them both answer without one, and the
|
|
15590
|
-
// client reads absence as "staging is not available here".
|
|
15591
|
-
...(patchGroupIdFromStore !== undefined ? {
|
|
15592
|
-
patchGroupId: patchGroupIdFromStore
|
|
15593
|
-
} : {}),
|
|
15594
|
-
...(headVersion !== undefined ? {
|
|
15595
|
-
headVersion
|
|
15596
|
-
} : {})
|
|
15597
|
-
}
|
|
15656
|
+
json: savedSoFar()
|
|
15598
15657
|
};
|
|
15599
15658
|
},
|
|
15600
15659
|
GET: async req => {
|
|
@@ -5639,6 +5639,11 @@ class ValOps {
|
|
|
5639
5639
|
patchGroup) {
|
|
5640
5640
|
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId, patchGroup);
|
|
5641
5641
|
if (fp.result.isErr(saveRes)) {
|
|
5642
|
+
if (saveRes.error.errorType === "plan-refused") {
|
|
5643
|
+
// The project's plan, not a failure: the Studio keeps the edit and
|
|
5644
|
+
// sends it again later, so an error line here would be one a minute.
|
|
5645
|
+
return fp.result.err(saveRes.error);
|
|
5646
|
+
}
|
|
5642
5647
|
console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
|
|
5643
5648
|
if (saveRes.error.errorType === "patch-head-conflict") {
|
|
5644
5649
|
return fp.result.err({
|
|
@@ -5965,6 +5970,16 @@ function isFileSource(value) {
|
|
|
5965
5970
|
* after the fact.
|
|
5966
5971
|
*/
|
|
5967
5972
|
|
|
5973
|
+
/**
|
|
5974
|
+
* The content API refused a write for the project's plan (a 403 that says why
|
|
5975
|
+
* as data), as opposed to refusing the patch.
|
|
5976
|
+
*
|
|
5977
|
+
* Its own error rather than an `other`: an `other` becomes a 400, and the
|
|
5978
|
+
* Studio drops a patch it is told is bad. This one is good, and is saved once
|
|
5979
|
+
* the plan allows it. `error` has the same shape as `other`'s so a caller that
|
|
5980
|
+
* only wants a sentence reads it the same way.
|
|
5981
|
+
*/
|
|
5982
|
+
|
|
5968
5983
|
function formatPatchSourceError(error) {
|
|
5969
5984
|
if ("message" in error) {
|
|
5970
5985
|
return error.message;
|
|
@@ -9371,6 +9386,11 @@ function isJsonValue(value) {
|
|
|
9371
9386
|
}
|
|
9372
9387
|
|
|
9373
9388
|
const textEncoder$2 = new TextEncoder();
|
|
9389
|
+
|
|
9390
|
+
/** The body of the content API's 403 for a write the project's plan refuses. */
|
|
9391
|
+
const PlanRefusedBody = z.z.object({
|
|
9392
|
+
error: internal.PlanRefusalError
|
|
9393
|
+
});
|
|
9374
9394
|
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
9375
9395
|
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
9376
9396
|
z.z.string().refine(s => !!s); // TODO: validate
|
|
@@ -11395,6 +11415,22 @@ class ValOpsHttp extends ValOps {
|
|
|
11395
11415
|
if ((_res$headers$get2 = res.headers.get("Content-Type")) !== null && _res$headers$get2 !== void 0 && _res$headers$get2.includes("application/json")) {
|
|
11396
11416
|
const json = await res.json();
|
|
11397
11417
|
const message = internal.getErrorMessageFromUnknownJson(json, "Unknown error");
|
|
11418
|
+
/*
|
|
11419
|
+
* Refused for the plan: the patch is fine, and the reason comes as
|
|
11420
|
+
* data. Kept apart from every other error, which the Studio is told
|
|
11421
|
+
* is a bad patch and drops. A 403 without a refusal in it (a
|
|
11422
|
+
* credential missing the write scope) is still one of those.
|
|
11423
|
+
*/
|
|
11424
|
+
const refusal = res.status === 403 ? PlanRefusedBody.safeParse(json) : null;
|
|
11425
|
+
if (refusal !== null && refusal !== void 0 && refusal.success) {
|
|
11426
|
+
return fp.result.err({
|
|
11427
|
+
errorType: "plan-refused",
|
|
11428
|
+
error: {
|
|
11429
|
+
message
|
|
11430
|
+
},
|
|
11431
|
+
refusal: refusal.data.error
|
|
11432
|
+
});
|
|
11433
|
+
}
|
|
11398
11434
|
return fp.result.err({
|
|
11399
11435
|
errorType: "other",
|
|
11400
11436
|
message
|
|
@@ -15541,6 +15577,24 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15541
15577
|
* `fs` mode and from an older content API.
|
|
15542
15578
|
*/
|
|
15543
15579
|
let headVersion;
|
|
15580
|
+
/*
|
|
15581
|
+
* What this request has saved so far: the whole answer to a save, and
|
|
15582
|
+
* the saved part of one refused for the plan. One builder for both, so
|
|
15583
|
+
* the client records a partly saved batch exactly as a saved one.
|
|
15584
|
+
*/
|
|
15585
|
+
const savedSoFar = () => ({
|
|
15586
|
+
newPatchIds,
|
|
15587
|
+
parentRef,
|
|
15588
|
+
// Absent rather than null where there are no groups: `fs` mode and
|
|
15589
|
+
// a content API that predates them both answer without one, and the
|
|
15590
|
+
// client reads absence as "staging is not available here".
|
|
15591
|
+
...(patchGroupIdFromStore !== undefined ? {
|
|
15592
|
+
patchGroupId: patchGroupIdFromStore
|
|
15593
|
+
} : {}),
|
|
15594
|
+
...(headVersion !== undefined ? {
|
|
15595
|
+
headVersion
|
|
15596
|
+
} : {})
|
|
15597
|
+
});
|
|
15544
15598
|
for (const patch of patches) {
|
|
15545
15599
|
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId, patchGroup);
|
|
15546
15600
|
if (fp.result.isErr(createPatchRes)) {
|
|
@@ -15552,6 +15606,23 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15552
15606
|
message: "The change was written against a different starting point than the server has."
|
|
15553
15607
|
}
|
|
15554
15608
|
};
|
|
15609
|
+
} else if (createPatchRes.error.errorType === "plan-refused") {
|
|
15610
|
+
/*
|
|
15611
|
+
* Val Build refused it for the plan. Passed on as its 403 rather
|
|
15612
|
+
* than a patch error: the patch is fine and the Studio keeps it,
|
|
15613
|
+
* and the reason travels as data so the fix can sit beside it.
|
|
15614
|
+
* The patches before this one in the batch ARE saved, and are
|
|
15615
|
+
* reported as a 200 would report them.
|
|
15616
|
+
*/
|
|
15617
|
+
return {
|
|
15618
|
+
status: 403,
|
|
15619
|
+
json: {
|
|
15620
|
+
type: "plan-refused",
|
|
15621
|
+
message: createPatchRes.error.error.message,
|
|
15622
|
+
error: createPatchRes.error.refusal,
|
|
15623
|
+
...savedSoFar()
|
|
15624
|
+
}
|
|
15625
|
+
};
|
|
15555
15626
|
} else {
|
|
15556
15627
|
return {
|
|
15557
15628
|
status: 400,
|
|
@@ -15582,19 +15653,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15582
15653
|
}
|
|
15583
15654
|
return {
|
|
15584
15655
|
status: 200,
|
|
15585
|
-
json:
|
|
15586
|
-
newPatchIds,
|
|
15587
|
-
parentRef,
|
|
15588
|
-
// Absent rather than null where there are no groups: `fs` mode and
|
|
15589
|
-
// a content API that predates them both answer without one, and the
|
|
15590
|
-
// client reads absence as "staging is not available here".
|
|
15591
|
-
...(patchGroupIdFromStore !== undefined ? {
|
|
15592
|
-
patchGroupId: patchGroupIdFromStore
|
|
15593
|
-
} : {}),
|
|
15594
|
-
...(headVersion !== undefined ? {
|
|
15595
|
-
headVersion
|
|
15596
|
-
} : {})
|
|
15597
|
-
}
|
|
15656
|
+
json: savedSoFar()
|
|
15598
15657
|
};
|
|
15599
15658
|
},
|
|
15600
15659
|
GET: async req => {
|
|
@@ -8,7 +8,7 @@ import path__default from 'path';
|
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import vm from 'node:vm';
|
|
10
10
|
import { Module } from 'node:module';
|
|
11
|
-
import { isSchemaSourceFixError, resolveSchemaSourceFixForError, Patch, getErrorMessageFromUnknownJson, PatchId as PatchId$1, JSONValue, PatchGroup, newestCommitSha, SerializedSchema, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, parseJob, VAL_STUDIO_HEADER, Api, traverseSchemaSource, richTextImageSchema, forEachRichTextImage } from '@valbuild/shared/internal';
|
|
11
|
+
import { isSchemaSourceFixError, resolveSchemaSourceFixForError, Patch, getErrorMessageFromUnknownJson, PlanRefusalError, PatchId as PatchId$1, JSONValue, PatchGroup, newestCommitSha, SerializedSchema, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, parseJob, VAL_STUDIO_HEADER, Api, traverseSchemaSource, richTextImageSchema, forEachRichTextImage } from '@valbuild/shared/internal';
|
|
12
12
|
import { createUIRequestHandler } from '@valbuild/ui/server';
|
|
13
13
|
import crypto$1 from 'crypto';
|
|
14
14
|
import z$1, { z } from 'zod';
|
|
@@ -5602,6 +5602,11 @@ class ValOps {
|
|
|
5602
5602
|
patchGroup) {
|
|
5603
5603
|
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId, patchGroup);
|
|
5604
5604
|
if (result.isErr(saveRes)) {
|
|
5605
|
+
if (saveRes.error.errorType === "plan-refused") {
|
|
5606
|
+
// The project's plan, not a failure: the Studio keeps the edit and
|
|
5607
|
+
// sends it again later, so an error line here would be one a minute.
|
|
5608
|
+
return result.err(saveRes.error);
|
|
5609
|
+
}
|
|
5605
5610
|
console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
|
|
5606
5611
|
if (saveRes.error.errorType === "patch-head-conflict") {
|
|
5607
5612
|
return result.err({
|
|
@@ -5928,6 +5933,16 @@ function isFileSource(value) {
|
|
|
5928
5933
|
* after the fact.
|
|
5929
5934
|
*/
|
|
5930
5935
|
|
|
5936
|
+
/**
|
|
5937
|
+
* The content API refused a write for the project's plan (a 403 that says why
|
|
5938
|
+
* as data), as opposed to refusing the patch.
|
|
5939
|
+
*
|
|
5940
|
+
* Its own error rather than an `other`: an `other` becomes a 400, and the
|
|
5941
|
+
* Studio drops a patch it is told is bad. This one is good, and is saved once
|
|
5942
|
+
* the plan allows it. `error` has the same shape as `other`'s so a caller that
|
|
5943
|
+
* only wants a sentence reads it the same way.
|
|
5944
|
+
*/
|
|
5945
|
+
|
|
5931
5946
|
function formatPatchSourceError(error) {
|
|
5932
5947
|
if ("message" in error) {
|
|
5933
5948
|
return error.message;
|
|
@@ -9334,6 +9349,11 @@ function isJsonValue(value) {
|
|
|
9334
9349
|
}
|
|
9335
9350
|
|
|
9336
9351
|
const textEncoder$2 = new TextEncoder();
|
|
9352
|
+
|
|
9353
|
+
/** The body of the content API's 403 for a write the project's plan refuses. */
|
|
9354
|
+
const PlanRefusedBody = z.object({
|
|
9355
|
+
error: PlanRefusalError
|
|
9356
|
+
});
|
|
9337
9357
|
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
|
9338
9358
|
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
|
9339
9359
|
z.string().refine(s => !!s); // TODO: validate
|
|
@@ -11358,6 +11378,22 @@ class ValOpsHttp extends ValOps {
|
|
|
11358
11378
|
if ((_res$headers$get2 = res.headers.get("Content-Type")) !== null && _res$headers$get2 !== void 0 && _res$headers$get2.includes("application/json")) {
|
|
11359
11379
|
const json = await res.json();
|
|
11360
11380
|
const message = getErrorMessageFromUnknownJson(json, "Unknown error");
|
|
11381
|
+
/*
|
|
11382
|
+
* Refused for the plan: the patch is fine, and the reason comes as
|
|
11383
|
+
* data. Kept apart from every other error, which the Studio is told
|
|
11384
|
+
* is a bad patch and drops. A 403 without a refusal in it (a
|
|
11385
|
+
* credential missing the write scope) is still one of those.
|
|
11386
|
+
*/
|
|
11387
|
+
const refusal = res.status === 403 ? PlanRefusedBody.safeParse(json) : null;
|
|
11388
|
+
if (refusal !== null && refusal !== void 0 && refusal.success) {
|
|
11389
|
+
return result.err({
|
|
11390
|
+
errorType: "plan-refused",
|
|
11391
|
+
error: {
|
|
11392
|
+
message
|
|
11393
|
+
},
|
|
11394
|
+
refusal: refusal.data.error
|
|
11395
|
+
});
|
|
11396
|
+
}
|
|
11361
11397
|
return result.err({
|
|
11362
11398
|
errorType: "other",
|
|
11363
11399
|
message
|
|
@@ -15504,6 +15540,24 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15504
15540
|
* `fs` mode and from an older content API.
|
|
15505
15541
|
*/
|
|
15506
15542
|
let headVersion;
|
|
15543
|
+
/*
|
|
15544
|
+
* What this request has saved so far: the whole answer to a save, and
|
|
15545
|
+
* the saved part of one refused for the plan. One builder for both, so
|
|
15546
|
+
* the client records a partly saved batch exactly as a saved one.
|
|
15547
|
+
*/
|
|
15548
|
+
const savedSoFar = () => ({
|
|
15549
|
+
newPatchIds,
|
|
15550
|
+
parentRef,
|
|
15551
|
+
// Absent rather than null where there are no groups: `fs` mode and
|
|
15552
|
+
// a content API that predates them both answer without one, and the
|
|
15553
|
+
// client reads absence as "staging is not available here".
|
|
15554
|
+
...(patchGroupIdFromStore !== undefined ? {
|
|
15555
|
+
patchGroupId: patchGroupIdFromStore
|
|
15556
|
+
} : {}),
|
|
15557
|
+
...(headVersion !== undefined ? {
|
|
15558
|
+
headVersion
|
|
15559
|
+
} : {})
|
|
15560
|
+
});
|
|
15507
15561
|
for (const patch of patches) {
|
|
15508
15562
|
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId, patchGroup);
|
|
15509
15563
|
if (result.isErr(createPatchRes)) {
|
|
@@ -15515,6 +15569,23 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15515
15569
|
message: "The change was written against a different starting point than the server has."
|
|
15516
15570
|
}
|
|
15517
15571
|
};
|
|
15572
|
+
} else if (createPatchRes.error.errorType === "plan-refused") {
|
|
15573
|
+
/*
|
|
15574
|
+
* Val Build refused it for the plan. Passed on as its 403 rather
|
|
15575
|
+
* than a patch error: the patch is fine and the Studio keeps it,
|
|
15576
|
+
* and the reason travels as data so the fix can sit beside it.
|
|
15577
|
+
* The patches before this one in the batch ARE saved, and are
|
|
15578
|
+
* reported as a 200 would report them.
|
|
15579
|
+
*/
|
|
15580
|
+
return {
|
|
15581
|
+
status: 403,
|
|
15582
|
+
json: {
|
|
15583
|
+
type: "plan-refused",
|
|
15584
|
+
message: createPatchRes.error.error.message,
|
|
15585
|
+
error: createPatchRes.error.refusal,
|
|
15586
|
+
...savedSoFar()
|
|
15587
|
+
}
|
|
15588
|
+
};
|
|
15518
15589
|
} else {
|
|
15519
15590
|
return {
|
|
15520
15591
|
status: 400,
|
|
@@ -15545,19 +15616,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
15545
15616
|
}
|
|
15546
15617
|
return {
|
|
15547
15618
|
status: 200,
|
|
15548
|
-
json:
|
|
15549
|
-
newPatchIds,
|
|
15550
|
-
parentRef,
|
|
15551
|
-
// Absent rather than null where there are no groups: `fs` mode and
|
|
15552
|
-
// a content API that predates them both answer without one, and the
|
|
15553
|
-
// client reads absence as "staging is not available here".
|
|
15554
|
-
...(patchGroupIdFromStore !== undefined ? {
|
|
15555
|
-
patchGroupId: patchGroupIdFromStore
|
|
15556
|
-
} : {}),
|
|
15557
|
-
...(headVersion !== undefined ? {
|
|
15558
|
-
headVersion
|
|
15559
|
-
} : {})
|
|
15560
|
-
}
|
|
15619
|
+
json: savedSoFar()
|
|
15561
15620
|
};
|
|
15562
15621
|
},
|
|
15563
15622
|
GET: async req => {
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"types": "dist/valbuild-server.cjs.d.ts",
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.145.0",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prettier/sync": "^0.6.1",
|
|
22
22
|
"@types/jest": "^30.0.0",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"typescript": "^6.0.3",
|
|
31
31
|
"zod": "^4.4.3",
|
|
32
32
|
"zod-validation-error": "^5.0.0",
|
|
33
|
-
"@valbuild/
|
|
34
|
-
"@valbuild/
|
|
35
|
-
"@valbuild/
|
|
33
|
+
"@valbuild/core": "0.145.0",
|
|
34
|
+
"@valbuild/shared": "0.145.0",
|
|
35
|
+
"@valbuild/ui": "0.145.0"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": "^20.19.0 || >=22"
|