@valbuild/server 0.136.0 → 0.136.2

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 CHANGED
@@ -1,5 +1,42 @@
1
1
  # @valbuild/server
2
2
 
3
+ ## 0.136.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#720](https://github.com/valbuild/val/pull/720) [`f76cd56`](https://github.com/valbuild/val/commit/f76cd56b9f1c1bcc0a608a7a4de24b1e776adccb) Thanks [@freekh](https://github.com/freekh)! - A managed project published from the Studio keeps its images, and a project created from a template can publish from the Studio at all.
8
+
9
+ - A publish from the Studio carries over every file under `public/` that the site already serves, so the favicon and images no longer disappear after the first Studio publish. When content published the live build, it already holds those files and nothing is uploaded again. When it didn't (a project cloned from a template), the Studio reads them from the site it runs on and builds with them.
10
+ - An image uploaded in the Studio is now part of the build that publishes it. For a managed project, `/save` returns the files the commit wrote (`binaryFiles`), the same way it already returns the `.val.ts` text.
11
+ - `/save` also returns the project's branch for a managed project, and the Studio's build uses it. A project cloned from a template was wired at no commit and no branch, so its first Studio publish went out branchless and the platform refused it.
12
+ - When neither content nor the platform can say which public files the live site serves, the Studio refuses to publish and says so, rather than publishing a site without them.
13
+
14
+ - Updated dependencies [[`f76cd56`](https://github.com/valbuild/val/commit/f76cd56b9f1c1bcc0a608a7a4de24b1e776adccb)]:
15
+ - @valbuild/ui@0.136.2
16
+ - @valbuild/shared@0.136.2
17
+
18
+ ## 0.136.1
19
+
20
+ ### Patch Changes
21
+
22
+ - [#716](https://github.com/valbuild/val/pull/716) [`4450be5`](https://github.com/valbuild/val/commit/4450be570ec3a66d7d98ba273c333ca2d2f163d9) Thanks [@freekh](https://github.com/freekh)! - `rebakeGit` rewires a project record to a different commit, and the Studio's publish proxy can now reach `/project-source`.
23
+
24
+ Both are groundwork for a publish that happens in the browser. `rebakeGit` replaces the one line in the generated `val.server.ts` that carries the commit, rather than regenerating the file: a publisher running inside the deployment does not know the project id or the platform's address, and reconstructing the file from what it could guess would throw away wiring it cannot see.
25
+
26
+ - [#716](https://github.com/valbuild/val/pull/716) [`d2f385a`](https://github.com/valbuild/val/commit/d2f385a04978992a5036379235e447ba7775faef) Thanks [@freekh](https://github.com/freekh)! - The Studio works again when served from the published package, and a managed project's publish now puts the saved edit on the site.
27
+
28
+ - **The Studio did not start in 0.136.0.** Its main bundle imports sibling chunks by relative path, and the Studio is served at `/api/val/static/<version>/app`, so those imports resolved to paths the handler did not know and came back as the HTML fallback — which a browser refuses as a module script. Preview was gone with it. The handler now serves those chunks, and the package build follows every import the bundle makes before it lets a release through.
29
+ - **The in-browser builder could not load.** The Studio's bundle carried rolldown's Node WASI binding, which throws `process is not defined` in a tab. It now uses the browser binding, and the build refuses a bundle that contains the Node one.
30
+ - **A managed publish builds what was just saved.** `/save` returns the source files the commit wrote, the Studio builds with them laid over the project's stored source, and publishes that source back with the build — so the next edit starts from this one rather than undoing it. The save's commit is also passed through to the build, which previously ran as though no commit had been made.
31
+ - **The bundler's WebAssembly is served from `content.val.build/v1/static`** (`DEFAULT_STATIC_HOST`), still addressed by its SHA-256. `globalThis.__VAL_ROLLDOWN_WASM_URL__` still overrides it.
32
+ - The `val.server.ts` that `@valbuild/tanstack-build` generates no longer hands a save's files to the platform (`platform.internal/__api/source`). A managed project's Studio builds in its own tab, and the platform has closed that door. `isWired` recognises files wired either way.
33
+ - `@valbuild/tanstack-build/constants` exports the contract paths and the artifact key namespace from an entrypoint that imports nothing else, for callers that must not load the bundler. The namespace gains a `source` key.
34
+
35
+ - Updated dependencies [[`cf89e74`](https://github.com/valbuild/val/commit/cf89e7429a79c4304ecbedd4f8b571a0de0f145f), [`c219574`](https://github.com/valbuild/val/commit/c21957498f3c7f4f47cef197c5dc0d591aa744ca), [`e673b43`](https://github.com/valbuild/val/commit/e673b43feaf48b7f30881d6786c858a0cb6a44a2), [`5c82928`](https://github.com/valbuild/val/commit/5c82928992d29f133f81cf0704a273c0449b18f1), [`d2f385a`](https://github.com/valbuild/val/commit/d2f385a04978992a5036379235e447ba7775faef)]:
36
+ - @valbuild/ui@0.136.1
37
+ - @valbuild/shared@0.136.1
38
+ - @valbuild/core@0.136.1
39
+
3
40
  ## 0.136.0
4
41
 
5
42
  ### Minor Changes
@@ -433,6 +433,43 @@ export declare abstract class ValOps {
433
433
  * an older service.
434
434
  */
435
435
  sourceMode(): "managed" | "connected" | null;
436
+ /**
437
+ * The branch the content service keeps this project's commits on, or `null`
438
+ * where there is no such service, or it has not said yet.
439
+ *
440
+ * For the Studio's in-tab build of a managed project, which has to name the
441
+ * branch its build was made at. Its other source is the `val.server.ts` the
442
+ * last build was wired with -- and a project cloned from a template seed has
443
+ * one wired at NO commit and no branch, so its first publish went out
444
+ * branchless and the loader refused it for a project whose pointer follows a
445
+ * branch. This is the project's own word for it.
446
+ */
447
+ projectBranch(): string | null;
448
+ /**
449
+ * Forward one call to the content service's publish API, as this project.
450
+ *
451
+ * The Studio builds a managed project in the tab and then has to publish what
452
+ * it built -- but a browser cannot talk to content directly. It holds a
453
+ * session cookie for THIS origin and no credential content would accept, so
454
+ * the conversation goes through here, exactly as patches already do.
455
+ *
456
+ * Refused by default, and that is the honest answer rather than a gap: `fs`
457
+ * and memory mode have no content service to forward to, and publishing there
458
+ * is writing to disk.
459
+ *
460
+ * The path is content's, minus the `/v1` prefix -- `/publish`,
461
+ * `/publish/{id}/artifacts`, `/build-target`. The ALLOW LIST lives in the
462
+ * implementation rather than here, because it is what keeps this from being
463
+ * a way to reach the rest of content with the project's credential.
464
+ */
465
+ publishApi(_path: string, _init: {
466
+ method: string;
467
+ body?: string;
468
+ }): Promise<{
469
+ status: number;
470
+ body: string;
471
+ contentType: string;
472
+ }>;
436
473
  /**
437
474
  * Whether a commit here produces `.val.ts` TEXT as well as data.
438
475
  *
@@ -141,6 +141,69 @@ export declare class ValOpsHttp extends ValOps {
141
141
  * has just arrived.
142
142
  */
143
143
  sourceMode(): "managed" | "connected" | null;
144
+ /** Remembered with {@link sourceMode}, from the same response. */
145
+ projectBranch(): string | null;
146
+ /**
147
+ * The short-lived publish token, and when it stops being usable.
148
+ *
149
+ * `null` until something publishes, which for most deployments is never.
150
+ */
151
+ private publishToken;
152
+ /**
153
+ * Trade this deployment's credential for one that can do one thing.
154
+ *
155
+ * Content's publish API takes a PROJECT TOKEN and nothing else --
156
+ * `authenticateProjectToken` refuses anything that is not one, deliberately,
157
+ * because a personal access token is a person's credential and does not name
158
+ * a project. What this deployment holds is the project's api key, which is
159
+ * neither.
160
+ *
161
+ * `POST /v1/{org}/{project}/publish-token` is the exchange, and it was built
162
+ * for exactly this: its own docblock names the case where "the caller was the
163
+ * project's api key ... a machine exchanging one machine credential for a
164
+ * narrower one". What comes back can publish one project for ten minutes.
165
+ *
166
+ * So the api key never leaves this process and the browser never sees any
167
+ * credential at all. That is the point of routing the publish through here
168
+ * rather than letting the tab talk to content.
169
+ */
170
+ private mintPublishToken;
171
+ /**
172
+ * A usable publish token, minting one when what we have will not last.
173
+ *
174
+ * The margin is what stops a token that is valid when the publish starts from
175
+ * expiring in the middle of it: a publish is five calls and an upload of
176
+ * every artifact, and the upload is the slow one.
177
+ */
178
+ private currentPublishToken;
179
+ /**
180
+ * The content paths this may reach, and nothing else.
181
+ *
182
+ * An allow list rather than a prefix check, because this method holds a
183
+ * credential and the browser chooses the path. `/publish/{id}` and its three
184
+ * steps are the publish conversation; `/build-target` is what a build needs
185
+ * to know before it starts; `/project-source` is what it builds.
186
+ *
187
+ * `/project-source` is here rather than on a route of its own because it is
188
+ * one of the three things a publish asks for and none of them are useful
189
+ * apart -- and because the credential is the same one. The Studio cannot get
190
+ * the project's files any other way: it runs inside the deployment, which
191
+ * holds a session for its own origin and an api key for content, and content
192
+ * is the only thing it is allowed to talk to at all.
193
+ *
194
+ * A publish id is opaque and content-generated, so it is matched rather than
195
+ * parsed -- what matters is that nothing with a `..`, a query or another
196
+ * segment gets through.
197
+ */
198
+ private static publishApiPathAllowed;
199
+ publishApi(path: string, init: {
200
+ method: string;
201
+ body?: string;
202
+ }): Promise<{
203
+ status: number;
204
+ body: string;
205
+ contentType: string;
206
+ }>;
144
207
  onInit(): Promise<void>;
145
208
  getPresignedAuthNonce(profileId: string, corsOrigin: string): Promise<{
146
209
  status: "success";
@@ -4595,6 +4595,48 @@ class ValOps {
4595
4595
  return null;
4596
4596
  }
4597
4597
 
4598
+ /**
4599
+ * The branch the content service keeps this project's commits on, or `null`
4600
+ * where there is no such service, or it has not said yet.
4601
+ *
4602
+ * For the Studio's in-tab build of a managed project, which has to name the
4603
+ * branch its build was made at. Its other source is the `val.server.ts` the
4604
+ * last build was wired with -- and a project cloned from a template seed has
4605
+ * one wired at NO commit and no branch, so its first publish went out
4606
+ * branchless and the loader refused it for a project whose pointer follows a
4607
+ * branch. This is the project's own word for it.
4608
+ */
4609
+ projectBranch() {
4610
+ return null;
4611
+ }
4612
+
4613
+ /**
4614
+ * Forward one call to the content service's publish API, as this project.
4615
+ *
4616
+ * The Studio builds a managed project in the tab and then has to publish what
4617
+ * it built -- but a browser cannot talk to content directly. It holds a
4618
+ * session cookie for THIS origin and no credential content would accept, so
4619
+ * the conversation goes through here, exactly as patches already do.
4620
+ *
4621
+ * Refused by default, and that is the honest answer rather than a gap: `fs`
4622
+ * and memory mode have no content service to forward to, and publishing there
4623
+ * is writing to disk.
4624
+ *
4625
+ * The path is content's, minus the `/v1` prefix -- `/publish`,
4626
+ * `/publish/{id}/artifacts`, `/build-target`. The ALLOW LIST lives in the
4627
+ * implementation rather than here, because it is what keeps this from being
4628
+ * a way to reach the rest of content with the project's credential.
4629
+ */
4630
+ async publishApi(_path, _init) {
4631
+ return {
4632
+ status: 501,
4633
+ contentType: "application/json",
4634
+ body: JSON.stringify({
4635
+ message: "This Val server has no content service to publish through. " + "Publishing from the browser is for a project whose content is " + "served over HTTP."
4636
+ })
4637
+ };
4638
+ }
4639
+
4598
4640
  /**
4599
4641
  * Whether a commit here produces `.val.ts` TEXT as well as data.
4600
4642
  *
@@ -7492,6 +7534,42 @@ class FSOpsHost {
7492
7534
  }
7493
7535
  }
7494
7536
 
7537
+ /**
7538
+ * The bytes of the local binary files a commit is about to write, base64.
7539
+ *
7540
+ * For the Studio's in-tab build of a managed project, which publishes the site:
7541
+ * an image uploaded in this save is in no earlier build, so unless the build
7542
+ * is given it the published page links a file that 404s. Remote files are left
7543
+ * out -- they are served from their own URL and never were part of a build.
7544
+ *
7545
+ * A file that cannot be read is NAMED rather than dropped, and does not stop
7546
+ * the save: the commit is what makes an edit durable, and the build is the
7547
+ * step that can refuse.
7548
+ *
7549
+ * Read BEFORE the commit, because after it the files are no longer the
7550
+ * patch's to read. Generic over the patch id so it asks for exactly the one
7551
+ * method it uses, and a test can hand it a plain object.
7552
+ */
7553
+ async function readCommittedBinaryFiles(ops, descriptors) {
7554
+ const files = {};
7555
+ const unread = [];
7556
+ await Promise.all(Object.entries(descriptors).map(async ([filePath, {
7557
+ patchId,
7558
+ remote
7559
+ }]) => {
7560
+ if (remote) return;
7561
+ const bytes = await ops.getBase64EncodedBinaryFileFromPatch(filePath, patchId, remote).catch(error => {
7562
+ console.error("Could not read a committed file", filePath, error);
7563
+ return null;
7564
+ });
7565
+ if (bytes === null) unread.push(filePath);else files[filePath] = bytes.toString("base64");
7566
+ }));
7567
+ return {
7568
+ files,
7569
+ unread: unread.sort()
7570
+ };
7571
+ }
7572
+
7495
7573
  /**
7496
7574
  * Which unpublished changes a failed save has to throw away to make progress.
7497
7575
  *
@@ -7973,6 +8051,189 @@ class ValOpsHttp extends ValOps {
7973
8051
  var _this$projectExpectat2;
7974
8052
  return ((_this$projectExpectat2 = this.projectExpectation) === null || _this$projectExpectat2 === void 0 ? void 0 : _this$projectExpectat2.sourceMode) ?? null;
7975
8053
  }
8054
+
8055
+ /** Remembered with {@link sourceMode}, from the same response. */
8056
+ projectBranch() {
8057
+ var _this$projectExpectat3;
8058
+ return ((_this$projectExpectat3 = this.projectExpectation) === null || _this$projectExpectat3 === void 0 ? void 0 : _this$projectExpectat3.branch) ?? null;
8059
+ }
8060
+
8061
+ /**
8062
+ * The short-lived publish token, and when it stops being usable.
8063
+ *
8064
+ * `null` until something publishes, which for most deployments is never.
8065
+ */
8066
+ publishToken = null;
8067
+
8068
+ /**
8069
+ * Trade this deployment's credential for one that can do one thing.
8070
+ *
8071
+ * Content's publish API takes a PROJECT TOKEN and nothing else --
8072
+ * `authenticateProjectToken` refuses anything that is not one, deliberately,
8073
+ * because a personal access token is a person's credential and does not name
8074
+ * a project. What this deployment holds is the project's api key, which is
8075
+ * neither.
8076
+ *
8077
+ * `POST /v1/{org}/{project}/publish-token` is the exchange, and it was built
8078
+ * for exactly this: its own docblock names the case where "the caller was the
8079
+ * project's api key ... a machine exchanging one machine credential for a
8080
+ * narrower one". What comes back can publish one project for ten minutes.
8081
+ *
8082
+ * So the api key never leaves this process and the browser never sees any
8083
+ * credential at all. That is the point of routing the publish through here
8084
+ * rather than letting the tab talk to content.
8085
+ */
8086
+ async mintPublishToken() {
8087
+ const res = await fetch(`${this.contentUrl}/v1/${this.project}/publish-token`, {
8088
+ method: "POST",
8089
+ headers: this.authHeaders
8090
+ });
8091
+ const text = await res.text();
8092
+ if (!res.ok) {
8093
+ return {
8094
+ status: res.status,
8095
+ error: `Could not get a publish token for '${this.project}': ` + `${res.status} ${text.slice(0, 300)}`
8096
+ };
8097
+ }
8098
+ let parsed;
8099
+ try {
8100
+ parsed = JSON.parse(text);
8101
+ } catch {
8102
+ return {
8103
+ status: 502,
8104
+ error: "The publish token exchange did not answer with JSON."
8105
+ };
8106
+ }
8107
+ const token = typeof parsed === "object" && parsed !== null && "token" in parsed && typeof parsed.token === "string" ? parsed.token : null;
8108
+ if (token === null) {
8109
+ return {
8110
+ status: 502,
8111
+ error: "The publish token exchange sent no token."
8112
+ };
8113
+ }
8114
+ const expiresAtRaw = typeof parsed === "object" && parsed !== null && "expiresAt" in parsed ? parsed.expiresAt : null;
8115
+ const expiresAt = typeof expiresAtRaw === "string" ? Date.parse(expiresAtRaw) : NaN;
8116
+ return {
8117
+ token,
8118
+ /*
8119
+ * A token with no expiry, or one we cannot read, is treated as expiring
8120
+ * NOW -- so it is used for this call and minted again for the next.
8121
+ * Caching one we cannot reason about is how a publish starts failing
8122
+ * halfway through, days later, for no reason anyone can see.
8123
+ */
8124
+ expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0
8125
+ };
8126
+ }
8127
+
8128
+ /**
8129
+ * A usable publish token, minting one when what we have will not last.
8130
+ *
8131
+ * The margin is what stops a token that is valid when the publish starts from
8132
+ * expiring in the middle of it: a publish is five calls and an upload of
8133
+ * every artifact, and the upload is the slow one.
8134
+ */
8135
+ async currentPublishToken() {
8136
+ const margin = 60_000;
8137
+ if (this.publishToken !== null && this.publishToken.expiresAt - margin > Date.now()) {
8138
+ return {
8139
+ token: this.publishToken.token
8140
+ };
8141
+ }
8142
+ const minted = await this.mintPublishToken();
8143
+ if ("error" in minted) {
8144
+ this.publishToken = null;
8145
+ return minted;
8146
+ }
8147
+ this.publishToken = minted;
8148
+ return {
8149
+ token: minted.token
8150
+ };
8151
+ }
8152
+
8153
+ /**
8154
+ * The content paths this may reach, and nothing else.
8155
+ *
8156
+ * An allow list rather than a prefix check, because this method holds a
8157
+ * credential and the browser chooses the path. `/publish/{id}` and its three
8158
+ * steps are the publish conversation; `/build-target` is what a build needs
8159
+ * to know before it starts; `/project-source` is what it builds.
8160
+ *
8161
+ * `/project-source` is here rather than on a route of its own because it is
8162
+ * one of the three things a publish asks for and none of them are useful
8163
+ * apart -- and because the credential is the same one. The Studio cannot get
8164
+ * the project's files any other way: it runs inside the deployment, which
8165
+ * holds a session for its own origin and an api key for content, and content
8166
+ * is the only thing it is allowed to talk to at all.
8167
+ *
8168
+ * A publish id is opaque and content-generated, so it is matched rather than
8169
+ * parsed -- what matters is that nothing with a `..`, a query or another
8170
+ * segment gets through.
8171
+ */
8172
+ static publishApiPathAllowed(path) {
8173
+ if (path === "/build-target" || path === "/project-source" || path === "/publish") {
8174
+ return true;
8175
+ }
8176
+ return /^\/publish\/[A-Za-z0-9_-]+(\/(artifacts|verify|promote))?$/.test(path);
8177
+ }
8178
+ async publishApi(path, init) {
8179
+ const json = "application/json";
8180
+ if (!ValOpsHttp.publishApiPathAllowed(path)) {
8181
+ return {
8182
+ status: 403,
8183
+ contentType: json,
8184
+ body: JSON.stringify({
8185
+ message: `'${path}' is not part of the publish API.`
8186
+ })
8187
+ };
8188
+ }
8189
+ const send = async token => fetch(`${this.contentUrl}/v1${path}`, {
8190
+ method: init.method,
8191
+ headers: {
8192
+ Authorization: `Bearer ${token}`,
8193
+ ...(init.body === undefined ? {} : {
8194
+ "Content-Type": json
8195
+ })
8196
+ },
8197
+ ...(init.body === undefined ? {} : {
8198
+ body: init.body
8199
+ })
8200
+ });
8201
+ const credential = await this.currentPublishToken();
8202
+ if ("error" in credential) {
8203
+ return {
8204
+ status: credential.status,
8205
+ contentType: json,
8206
+ body: JSON.stringify({
8207
+ message: credential.error
8208
+ })
8209
+ };
8210
+ }
8211
+ let res = await send(credential.token);
8212
+ if (res.status === 401) {
8213
+ /*
8214
+ * Revoked, or expired sooner than it said. One retry with a fresh token,
8215
+ * because the alternative is a publish that fails for a reason the editor
8216
+ * cannot act on and a retry that fails the same way.
8217
+ */
8218
+ this.publishToken = null;
8219
+ const second = await this.currentPublishToken();
8220
+ if ("error" in second) {
8221
+ return {
8222
+ status: second.status,
8223
+ contentType: json,
8224
+ body: JSON.stringify({
8225
+ message: second.error
8226
+ })
8227
+ };
8228
+ }
8229
+ res = await send(second.token);
8230
+ }
8231
+ return {
8232
+ status: res.status,
8233
+ contentType: res.headers.get("content-type") ?? json,
8234
+ body: await res.text()
8235
+ };
8236
+ }
7976
8237
  async onInit() {
7977
8238
  // TODO: unused for now. Implement or remove
7978
8239
  }
@@ -11531,6 +11792,38 @@ const ValServer = (valModules, options, callbacks) => {
11531
11792
  }
11532
11793
  };
11533
11794
  };
11795
+
11796
+ /**
11797
+ * Hand one publish-API call to content and carry the answer back.
11798
+ *
11799
+ * `JSON.parse` and nothing more. That is not validation -- there is no schema
11800
+ * here and no knowledge of content's shapes -- it is the round trip a JSON
11801
+ * body has to make to travel as `json` rather than as a string. A body that
11802
+ * does not parse is a gateway's error page rather than content answering, so
11803
+ * it comes back as a message with the original status instead of throwing:
11804
+ * a publish that failed is something the editor has to be told in words.
11805
+ */
11806
+ const proxyPublishApi = async (path, method, body) => {
11807
+ const answer = await serverOps.publishApi(path ?? "", {
11808
+ method,
11809
+ ...(body === undefined ? {} : {
11810
+ body
11811
+ })
11812
+ });
11813
+ try {
11814
+ return {
11815
+ status: answer.status,
11816
+ json: JSON.parse(answer.body)
11817
+ };
11818
+ } catch {
11819
+ return {
11820
+ status: answer.status,
11821
+ json: {
11822
+ message: answer.body.trim() === "" ? `The publish API answered ${answer.status} with no body.` : `The publish API answered ${answer.status}: ${answer.body.slice(0, 300)}`
11823
+ }
11824
+ };
11825
+ }
11826
+ };
11534
11827
  return {
11535
11828
  "/draft/enable": {
11536
11829
  GET: async req => {
@@ -11981,6 +12274,47 @@ const ValServer = (valModules, options, callbacks) => {
11981
12274
  };
11982
12275
  }
11983
12276
  },
12277
+ /**
12278
+ * The content service's publish API, reached through this deployment.
12279
+ *
12280
+ * The Studio builds a managed project in its own tab and then publishes
12281
+ * what it built. It cannot talk to content directly -- it holds a session
12282
+ * cookie for this origin and no credential content would accept -- so the
12283
+ * whole conversation comes through here.
12284
+ *
12285
+ * Three things happen, and nothing else: the session is checked, the
12286
+ * credential is swapped for a publish-scoped one (see
12287
+ * `ValOpsHttp.publishApi`), and content's answer is carried back with its
12288
+ * status intact. The Studio parses that answer with the same module
12289
+ * `val publish` uses, so there is no copy of content's shapes here to
12290
+ * fall out of step.
12291
+ */
12292
+ "/publish-api": {
12293
+ GET: async req => {
12294
+ const auth = getAuth(req.cookies);
12295
+ if (auth.error) {
12296
+ return {
12297
+ status: 401,
12298
+ json: {
12299
+ message: auth.error
12300
+ }
12301
+ };
12302
+ }
12303
+ return proxyPublishApi(req.path, "GET");
12304
+ },
12305
+ POST: async req => {
12306
+ const auth = getAuth(req.cookies);
12307
+ if (auth.error) {
12308
+ return {
12309
+ status: 401,
12310
+ json: {
12311
+ message: auth.error
12312
+ }
12313
+ };
12314
+ }
12315
+ return proxyPublishApi(req.path, "POST", req.body === undefined ? undefined : JSON.stringify(req.body));
12316
+ }
12317
+ },
11984
12318
  "/upload/patches": {
11985
12319
  POST: async req => {
11986
12320
  if (serverOps instanceof ValOpsHttp) {
@@ -13480,6 +13814,12 @@ const ValServer = (valModules, options, callbacks) => {
13480
13814
  }
13481
13815
  }
13482
13816
  const message = body.message || "Val CMS update (" + Object.keys(analysis.patchesByModule).length + " files changed)";
13817
+ /*
13818
+ * Before the commit, because after it the files are no longer the
13819
+ * patch's to read. See `binaryFiles` on the route.
13820
+ */
13821
+ const managedBranch = serverOps.sourceMode() === "managed" ? serverOps.projectBranch() : null;
13822
+ const committedBinaries = serverOps.sourceMode() === "managed" ? await readCommittedBinaryFiles(serverOps, preparedCommit.patchedBinaryFilesDescriptors) : null;
13483
13823
  const commitToGit = () => {
13484
13824
  var _options$config$files;
13485
13825
  return serverOps.commit(preparedCommit, message, auth.id, ((_options$config$files = options.config.files) === null || _options$config$files === void 0 ? void 0 : _options$config$files.directory) || "/public/val", undefined,
@@ -13538,7 +13878,24 @@ const ValServer = (valModules, options, callbacks) => {
13538
13878
  return {
13539
13879
  status: 200,
13540
13880
  json: {
13541
- commitSha: commitRes.commit
13881
+ commitSha: commitRes.commit,
13882
+ /*
13883
+ * For the Studio to BUILD, when it is the deployer. See
13884
+ * `sourceFiles` on the route: the build must contain the text
13885
+ * this commit wrote, and only this handler has it.
13886
+ */
13887
+ ...(serverOps.sourceMode() === "managed" ? {
13888
+ sourceFiles: preparedCommit.patchedSourceFiles
13889
+ } : {}),
13890
+ ...(managedBranch !== null ? {
13891
+ branch: managedBranch
13892
+ } : {}),
13893
+ ...(committedBinaries !== null ? {
13894
+ binaryFiles: committedBinaries.files,
13895
+ ...(committedBinaries.unread.length > 0 ? {
13896
+ binaryFilesUnread: committedBinaries.unread
13897
+ } : {})
13898
+ } : {})
13542
13899
  }
13543
13900
  };
13544
13901
  }