@blaxel/core 0.3.26 → 0.3.27-preview.317

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.
@@ -24,8 +24,8 @@ function missingCredentialsMessage() {
24
24
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
25
25
  }
26
26
  // Build info - these placeholders are replaced at build time by build:replace-imports
27
- const BUILD_VERSION = "0.3.26";
28
- const BUILD_COMMIT = "18c2c73526c47b9ebc09aa3e16497b19854a02d0";
27
+ const BUILD_VERSION = "0.3.27-preview.317";
28
+ const BUILD_COMMIT = "b818d0f5560b92040baeb620b8d89bf148cba56d";
29
29
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
30
30
  const BLAXEL_API_VERSION = "2026-09-22";
31
31
  // Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
@@ -692,6 +692,39 @@ export class ImageInstance {
692
692
  const uploadUrl = response.headers.get("x-blaxel-upload-url");
693
693
  return { response, uploadUrl };
694
694
  }
695
+ /**
696
+ * Creates or updates the sandbox and returns its upload URL with the build
697
+ * metadata that URL was signed for, plus requested settings it was not.
698
+ */
699
+ async _requestUpload(sandbox, requested) {
700
+ const { response, uploadUrl } = await this._createSandboxWithUpload(sandbox);
701
+ if (response.status >= 400) {
702
+ const text = await response.text();
703
+ throw new Error(`Failed to create sandbox: ${response.status} - ${text}`);
704
+ }
705
+ if (!uploadUrl) {
706
+ throw new Error("No upload URL returned from API");
707
+ }
708
+ // Sign-matching headers come from the labels the control plane stored,
709
+ // which can include build settings already on an existing sandbox.
710
+ let storedLabels = sandbox.metadata?.labels;
711
+ try {
712
+ const stored = (await response.json());
713
+ storedLabels = stored.metadata?.labels ?? storedLabels;
714
+ }
715
+ catch {
716
+ // Fall back to the labels we sent
717
+ }
718
+ // Storage rejects headers the URL was not signed for, so drop those.
719
+ let metadata = buildUploadMetadata(storedLabels);
720
+ const query = new URL(uploadUrl).searchParams;
721
+ const signedHeaders = query.get("X-Amz-SignedHeaders") ?? query.get("X-Goog-SignedHeaders");
722
+ if (signedHeaders) {
723
+ const signed = new Set(signedHeaders.split(";").map((header) => header.trim().toLowerCase()));
724
+ metadata = Object.fromEntries(Object.entries(metadata).filter(([name]) => signed.has(`x-amz-meta-${name}`)));
725
+ }
726
+ return { uploadUrl, metadata, missing: Object.keys(requested).filter((name) => !(name in metadata)) };
727
+ }
695
728
  async _uploadZip(uploadUrl, zipContent, metadata = {}) {
696
729
  const headers = {
697
730
  "Content-Type": "application/zip",
@@ -792,26 +825,17 @@ export class ImageInstance {
792
825
  // Create sandbox payload
793
826
  const sandboxPayload = this._createSandboxPayload(name, memory, buildLabels);
794
827
  // Create/update sandbox and get upload URL
795
- const { response, uploadUrl } = await this._createSandboxWithUpload(sandboxPayload);
796
- if (response.status >= 400) {
797
- const text = await response.text();
798
- throw new Error(`Failed to create sandbox: ${response.status} - ${text}`);
799
- }
800
- if (!uploadUrl) {
801
- throw new Error("No upload URL returned from API");
802
- }
803
- // Sign-matching headers come from the labels the control plane stored,
804
- // which can include build settings already on an existing sandbox.
805
- let storedLabels = sandboxPayload.metadata?.labels;
806
- try {
807
- const created = (await response.json());
808
- storedLabels = created.metadata?.labels ?? storedLabels;
809
- }
810
- catch {
811
- // Fall back to the labels we sent
828
+ let { uploadUrl, metadata, missing } = await this._requestUpload(sandboxPayload, buildLabels);
829
+ if (missing.length > 0) {
830
+ // Creating a sandbox can return a URL without the build settings
831
+ // signed; asking again updates the now existing sandbox, which does.
832
+ ({ uploadUrl, metadata, missing } = await this._requestUpload(sandboxPayload, buildLabels));
833
+ if (missing.length > 0) {
834
+ throw new Error(`The platform did not accept build settings ${missing.join(", ")}; retry without them`);
835
+ }
812
836
  }
813
837
  // Upload the zip
814
- await this._uploadZip(uploadUrl, zipContent, buildUploadMetadata(storedLabels));
838
+ await this._uploadZip(uploadUrl, zipContent, metadata);
815
839
  // Wait for deployment to complete
816
840
  await this._waitForDeployment(name, timeout, 3000, onStatusChange);
817
841
  // Get the final sandbox state
@@ -543,8 +543,12 @@ describe("Dockerfile instruction order", () => {
543
543
  });
544
544
  });
545
545
  describe("ImageInstance.build worker resources", () => {
546
- async function buildWith(options, storedLabels) {
547
- const calls = [];
546
+ // signing: "all" signs every build label, "update" signs only on PUT after a
547
+ // 404 then POST create (production today), "none" never signs them.
548
+ let calls = [];
549
+ async function buildWith(options, storedLabels, signing = "all") {
550
+ calls = [];
551
+ let created = false;
548
552
  vi.spyOn(settings, "authenticate").mockResolvedValue(undefined);
549
553
  vi.spyOn(settings, "headers", "get").mockReturnValue({});
550
554
  vi.spyOn(settings, "baseUrl", "get").mockReturnValue("https://api.test");
@@ -553,10 +557,18 @@ describe("ImageInstance.build worker resources", () => {
553
557
  const body = typeof init.body === "string" ? JSON.parse(init.body) : undefined;
554
558
  calls.push({ url, method: init.method ?? "GET", headers, body });
555
559
  if (url.includes("upload=true")) {
556
- const sent = body.metadata?.labels;
557
- return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img", labels: storedLabels ?? sent } }), {
560
+ if (signing === "update" && init.method === "PUT" && !created) {
561
+ return Promise.resolve(new Response("{}", { status: 404 }));
562
+ }
563
+ created ||= init.method === "POST";
564
+ const labels = storedLabels ?? body.metadata?.labels ?? {};
565
+ const signed = ["host"];
566
+ if (signing === "all" || (signing === "update" && init.method === "PUT")) {
567
+ signed.push(...Object.keys(labels).filter((k) => k.startsWith("x-blaxel-build")).map((k) => `x-amz-meta-${k}`));
568
+ }
569
+ return Promise.resolve(new Response(JSON.stringify({ metadata: { name: "img", labels } }), {
558
570
  status: 200,
559
- headers: { "x-blaxel-upload-url": "https://upload.test/object" },
571
+ headers: { "x-blaxel-upload-url": `https://upload.test/object?X-Amz-SignedHeaders=${signed.join(";")}` },
560
572
  }));
561
573
  }
562
574
  if (url.startsWith("https://upload.test")) {
@@ -589,6 +601,19 @@ describe("ImageInstance.build worker resources", () => {
589
601
  expect(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("32768");
590
602
  expect(upload.headers["x-amz-meta-x-blaxel-auto-generated"]).toBeUndefined();
591
603
  });
604
+ it("asks again when creating a sandbox does not sign the settings", async () => {
605
+ const { upload } = await buildWith({ buildMemoryMb: 2048 }, undefined, "update");
606
+ expect(calls.filter((call) => call.url.includes("upload=true")).map((call) => call.method)).toEqual(["PUT", "POST", "PUT"]);
607
+ expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBe("2048");
608
+ });
609
+ it("fails clearly when the settings are never signed", async () => {
610
+ await expect(buildWith({ buildVolumeMb: 32768 }, undefined, "none")).rejects.toThrow("x-blaxel-build-volume");
611
+ expect(calls.some((call) => call.url.startsWith("https://upload.test"))).toBe(false);
612
+ });
613
+ it("drops stored labels the upload URL was not signed for", async () => {
614
+ const { upload } = await buildWith({}, { "x-blaxel-auto-generated": "true", "x-blaxel-build-memory": "8192" }, "none");
615
+ expect(upload.headers["x-amz-meta-x-blaxel-build-memory"]).toBeUndefined();
616
+ });
592
617
  it("keeps an explicit zero volume", async () => {
593
618
  const { upload } = await buildWith({ buildVolumeMb: 0 });
594
619
  expect(upload.headers["x-amz-meta-x-blaxel-build-volume"]).toBe("0");